SQL и sqlite3

⏱ 55 мин · 🧩 заданий: 7

Реляционная модель

Реляционная база хранит данные в таблицах: строки — записи, колонки — поля с типами. Таблицы связываются через ключи: первичный ключ (PRIMARY KEY) однозначно определяет строку, внешний (FOREIGN KEY) ссылается на строку другой таблицы.

customers orders ┌────┬───────┬────────┐ ┌────┬─────────────┬────────┐ │ id │ name │ city │ │ id │ customer_id │ amount │ ├────┼───────┼────────┤ ├────┼─────────────┼────────┤ │ 1 │ Аня │ Казань │◀──┐ │ 1 │ 1 │ 500 │ │ 2 │ Борис │ Москва │◀─┐└───│ 2 │ 1 │ 250 │ │ 3 │ Вера │ Омск │ └────│ 3 │ 2 │ 900 │ └────┴───────┴────────┘ └────┴─────────────┴────────┘ один клиент — много заказов (связь «один ко многим»)

SQLite — база в одном файле, встроенная в Python (import sqlite3), не требует сервера. Её используют в мобильных приложениях, браузерах, тестах и прототипах. В продакшене веб-сервисов чаще PostgreSQL, но SQL почти одинаковый. Все примеры урока выполняются в настоящей SQLite прямо в браузере.

Первые запросы

import sqlite3

conn = sqlite3.connect(":memory:")          # база в памяти; для файла — "shop.db"
conn.executescript("""
    CREATE TABLE customers (
        id   INTEGER PRIMARY KEY,           -- в SQLite это автоинкремент rowid
        name TEXT NOT NULL,
        city TEXT
    );
    CREATE TABLE orders (
        id          INTEGER PRIMARY KEY,
        customer_id INTEGER NOT NULL REFERENCES customers(id),
        amount      INTEGER NOT NULL CHECK (amount > 0),
        created     TEXT NOT NULL           -- даты в SQLite хранят строками ISO 8601
    );
    INSERT INTO customers (name, city) VALUES
        ('Аня', 'Казань'), ('Борис', 'Москва'), ('Вера', 'Москва'), ('Гоша', 'Омск');
    INSERT INTO orders (customer_id, amount, created) VALUES
        (1, 500, '2026-01-10'), (1, 250, '2026-02-03'), (2, 900, '2026-02-15'),
        (3, 120, '2026-03-01'), (3, 700, '2026-03-20'), (2, 300, '2026-03-28');
""")

def show(sql, params=()):
    cur = conn.execute(sql, params)
    names = [d[0] for d in cur.description]
    rows = cur.fetchall()
    widths = [max(len(str(x)) for x in [n, *[r[i] for r in rows]]) for i, n in enumerate(names)]
    print(" | ".join(n.ljust(w) for n, w in zip(names, widths)))
    print("-+-".join("-" * w for w in widths))
    for r in rows:
        print(" | ".join(str(x).ljust(w) for x, w in zip(r, widths)))
    print()

show("SELECT * FROM customers")
show("SELECT name, city FROM customers WHERE city = 'Москва' ORDER BY name DESC")
show("SELECT id, amount FROM orders WHERE amount BETWEEN 200 AND 600 ORDER BY amount DESC LIMIT 2")
show("SELECT name FROM customers WHERE name LIKE 'В%' OR city IN ('Омск', 'Сочи')")

Порядок записи запроса: SELECT … FROM … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT. Порядок выполнения другой: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Поэтому псевдоним из SELECT нельзя использовать в WHERE, но можно в ORDER BY.

NULL

NULL — «значение неизвестно». Любое сравнение с ним даёт NULL (не истину), поэтому = NULL никогда не срабатывает.

import sqlite3
conn = sqlite3.connect(":memory:")
print(conn.execute("SELECT NULL = NULL, NULL IS NULL, 1 + NULL, COALESCE(NULL, 'по умолчанию')").fetchone())
conn.executescript("CREATE TABLE t (x); INSERT INTO t VALUES (1), (NULL), (3);")
print(conn.execute("SELECT count(*), count(x), sum(x), avg(x) FROM t").fetchone())   # агрегаты пропускают NULL
print(conn.execute("SELECT x FROM t WHERE x != 1").fetchall())                       # NULL не попал!

Агрегаты и GROUP BY

import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("""
    CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, city TEXT);
    CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER, created TEXT);
    INSERT INTO customers (name, city) VALUES ('Аня', 'Казань'), ('Борис', 'Москва'), ('Вера', 'Москва'), ('Гоша', 'Омск');
    INSERT INTO orders (customer_id, amount, created) VALUES (1, 500, '2026-01-10'), (1, 250, '2026-02-03'),
        (2, 900, '2026-02-15'), (3, 120, '2026-03-01'), (3, 700, '2026-03-20'), (2, 300, '2026-03-28');
""")

print(conn.execute("SELECT count(*), sum(amount), avg(amount), min(amount), max(amount) FROM orders").fetchone())

# по клиентам
for row in conn.execute("""
    SELECT customer_id, count(*) AS n, sum(amount) AS total
    FROM orders
    GROUP BY customer_id
    HAVING total > 800              -- фильтр по группам
    ORDER BY total DESC
"""):
    print(row)

# по месяцам
for row in conn.execute("SELECT substr(created, 1, 7) AS month, sum(amount) FROM orders GROUP BY month"):
    print(row)

WHERE против HAVING

WHERE отбирает строки до группировки, HAVING — группы после. Условие на агрегат (sum(amount) > 800) может быть только в HAVING.

JOIN

INNER JOIN — только пары, где есть совпадение LEFT JOIN — все строки левой таблицы; справа NULL, если пары нет RIGHT JOIN — наоборот (в SQLite с 3.39) FULL JOIN — все строки обеих таблиц CROSS JOIN — все со всеми (декартово произведение)
import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("""
    CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, city TEXT);
    CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER);
    INSERT INTO customers (name, city) VALUES ('Аня', 'Казань'), ('Борис', 'Москва'), ('Вера', 'Москва'), ('Гоша', 'Омск');
    INSERT INTO orders (customer_id, amount) VALUES (1, 500), (1, 250), (2, 900), (3, 120), (99, 50);
""")

print("INNER:", conn.execute("""
    SELECT c.name, o.amount FROM customers c JOIN orders o ON o.customer_id = c.id
""").fetchall())

print("LEFT: ", conn.execute("""
    SELECT c.name, o.amount FROM customers c LEFT JOIN orders o ON o.customer_id = c.id
""").fetchall())

print("без заказов:", conn.execute("""
    SELECT c.name FROM customers c LEFT JOIN orders o ON o.customer_id = c.id WHERE o.id IS NULL
""").fetchall())

print("FULL: ", conn.execute("""
    SELECT c.name, o.amount FROM customers c FULL JOIN orders o ON o.customer_id = c.id
""").fetchall())

print("итоги:", conn.execute("""
    SELECT c.name, count(o.id), coalesce(sum(o.amount), 0)
    FROM customers c LEFT JOIN orders o ON o.customer_id = c.id
    GROUP BY c.id ORDER BY 3 DESC
""").fetchall())

count(*) против count(колонка)

В LEFT JOIN у клиента без заказов одна строка с NULL справа. count(*) посчитает её (1), а count(o.id) — нет (0).

Подзапросы и CTE

import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("""
    CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER);
    INSERT INTO orders (customer_id, amount) VALUES (1, 500), (1, 250), (2, 900), (3, 120), (3, 700), (2, 300);
""")

# заказы дороже среднего
print(conn.execute("SELECT id, amount FROM orders WHERE amount > (SELECT avg(amount) FROM orders)").fetchall())

# CTE (WITH) — именованный подзапрос, читается сверху вниз
print(conn.execute("""
    WITH totals AS (
        SELECT customer_id, sum(amount) AS total FROM orders GROUP BY customer_id
    )
    SELECT customer_id, total FROM totals WHERE total = (SELECT max(total) FROM totals)
""").fetchall())

# рекурсивный CTE: числа от 1 до 5 (так обходят деревья: категории, комментарии, оргструктуру)
print(conn.execute("""
    WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM n WHERE x < 5)
    SELECT group_concat(x, ',') FROM n
""").fetchone())

Оконные функции

Агрегат по «окну» строк, не схлопывая их в одну. Любимая тема собеседований: «топ-N в каждой группе», «нарастающий итог», «разница с предыдущей строкой».

import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("""
    CREATE TABLE sales (manager TEXT, month TEXT, amount INTEGER);
    INSERT INTO sales VALUES
        ('Аня', '01', 100), ('Аня', '02', 150), ('Аня', '03', 90),
        ('Борис', '01', 200), ('Борис', '02', 120), ('Борис', '03', 210);
""")

for row in conn.execute("""
    SELECT manager, month, amount,
           sum(amount) OVER (PARTITION BY manager ORDER BY month)        AS running_total,
           amount - lag(amount) OVER (PARTITION BY manager ORDER BY month) AS diff,
           rank() OVER (ORDER BY amount DESC)                            AS place
    FROM sales
    ORDER BY manager, month
"""):
    print(row)

print("лучший месяц каждого менеджера:")
print(conn.execute("""
    SELECT manager, month, amount FROM (
        SELECT *, row_number() OVER (PARTITION BY manager ORDER BY amount DESC) AS rn FROM sales
    ) WHERE rn = 1
""").fetchall())

ROW_NUMBER нумерует подряд (1, 2, 3), RANK даёт одинаковым значениям одно место с пропуском (1, 1, 3), DENSE_RANK — без пропуска (1, 1, 2).

Изменение данных

import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE products (sku TEXT PRIMARY KEY, title TEXT, stock INTEGER DEFAULT 0)")

conn.executemany("INSERT INTO products (sku, title, stock) VALUES (?, ?, ?)",
                 [("A1", "Кружка", 10), ("B2", "Футболка", 0), ("C3", "Блокнот", 5)])
conn.execute("UPDATE products SET stock = stock - 1 WHERE sku = ?", ("A1",))
conn.execute("DELETE FROM products WHERE stock = 0")

# UPSERT: вставить или обновить при конфликте ключа
conn.execute("""
    INSERT INTO products (sku, title, stock) VALUES ('C3', 'Блокнот', 7)
    ON CONFLICT (sku) DO UPDATE SET stock = stock + excluded.stock
""")
print(conn.execute("SELECT * FROM products ORDER BY sku").fetchall())

row = conn.execute("INSERT INTO products (sku, title) VALUES ('D4', 'Ручка') RETURNING rowid, sku").fetchone()
print("вставлено:", row)

UPDATE и DELETE без WHERE

DELETE FROM products удалит всё. Перед опасным запросом выполните тот же SELECT … WHERE и посмотрите, что попадёт под удаление; работайте в транзакции.

Индексы

Без индекса поиск по условию — полный перебор таблицы. Индекс (обычно B-дерево) — отсортированная структура «значение → строки», поиск за O(log n).

import sqlite3, time
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER)")
conn.executemany("INSERT INTO orders (customer_id, amount) VALUES (?, ?)",
                 ((i % 5000, i % 997) for i in range(200_000)))

query = "SELECT count(*), sum(amount) FROM orders WHERE customer_id = 1234"

def measure():
    start = time.perf_counter()
    for _ in range(20):
        conn.execute(query).fetchone()
    return (time.perf_counter() - start) / 20 * 1000

print(conn.execute("EXPLAIN QUERY PLAN " + query).fetchall())
print(f"без индекса: {measure():.2f} мс")

conn.execute("CREATE INDEX ix_orders_customer ON orders (customer_id)")
print(conn.execute("EXPLAIN QUERY PLAN " + query).fetchall())
print(f"с индексом:  {measure():.3f} мс")

Что важно знать про индексы:

  • PRIMARY KEY и UNIQUE создают индекс автоматически; внешний ключ — нет (в PostgreSQL тоже), его индекс добавляют сами.
  • Составной индекс (a, b) помогает запросам по a и по a AND b, но не по одному b (правило левого префикса).
  • Индекс не поможет, если колонка обёрнута в функцию (WHERE lower(email) = …) или условие LIKE '%текст'.
  • Каждый индекс замедляет запись и занимает место. Создавайте их под реальные медленные запросы и проверяйте план (EXPLAIN / EXPLAIN ANALYZE в PostgreSQL).

Транзакции и ACID

Транзакция — группа операций, которая выполняется целиком или не выполняется вовсе.

  • Atomicity — атомарность: всё или ничего;
  • Consistency — согласованность: ограничения не нарушаются;
  • Isolation — изоляция: параллельные транзакции не видят «половинчатых» изменений друг друга;
  • Durability — долговечность: после COMMIT данные переживут сбой.
import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER CHECK (balance >= 0)); INSERT INTO accounts VALUES (1, 100), (2, 0);")

def transfer(amount):
    with conn:       # BEGIN … COMMIT, а при исключении — ROLLBACK
        conn.execute("UPDATE accounts SET balance = balance + ? WHERE id = 2", (amount,))
        conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = 1", (amount,))

transfer(30)
print(conn.execute("SELECT * FROM accounts").fetchall())
try:
    transfer(500)        # второе UPDATE нарушит CHECK — первое тоже откатится
except sqlite3.IntegrityError as e:
    print("ошибка:", e)
print(conn.execute("SELECT * FROM accounts").fetchall())

Уровни изоляции (SQL-стандарт, важны в PostgreSQL/MySQL) — компромисс между корректностью и параллельностью:

Уровень Грязное чтение Неповторяемое чтение Фантомы
Read Uncommitted возможно возможно возможны
Read Committed (по умолчанию в PostgreSQL) нет возможно возможны
Repeatable Read нет нет возможны*
Serializable нет нет нет

* в PostgreSQL на Repeatable Read фантомов тоже нет. Классическая гонка «прочитал остаток → проверил → записал» решается атомарным UPDATE … WHERE balance >= ?, блокировкой SELECT … FOR UPDATE (PostgreSQL) или уровнем Serializable с повтором транзакции.

Модуль sqlite3: главное

import sqlite3

conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row            # строки как словари: row["name"]
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
conn.executemany("INSERT INTO users (name, email) VALUES (:name, :email)", [
    {"name": "Аня", "email": "anya@example.com"},
    {"name": "Борис", "email": "boris@example.com"},
])
conn.commit()

row = conn.execute("SELECT * FROM users WHERE name = ?", ("Аня",)).fetchone()
print(row["id"], row["email"], dict(row))

for user in conn.execute("SELECT name FROM users ORDER BY name"):
    print(user["name"])

conn.close()
  • ? или :имя — параметры: значения передаются отдельно от текста запроса.
  • fetchone() / fetchall() / итерация по курсору (лениво, для больших выборок).
  • with conn: — транзакция; само соединение он не закрывает.

SQL-инъекции

import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("CREATE TABLE users (login TEXT, password_hash TEXT); INSERT INTO users VALUES ('admin', 'x'), ('anna', 'y');")

login = "' OR '1'='1"
unsafe = f"SELECT login FROM users WHERE login = '{login}'"
print(unsafe)
print("форматирование:", conn.execute(unsafe).fetchall())          # вернулись все!
print("параметры:     ", conn.execute("SELECT login FROM users WHERE login = ?", (login,)).fetchall())

Никогда не собирайте SQL из пользовательских данных через f-строки, % или +. Параметры нельзя использовать для имён таблиц и колонок — их выбирают из белого списка.

Проектирование: нормализация и связи

  • Один ко многим: внешний ключ на стороне «многих» (orders.customer_id).
  • Многие ко многим: промежуточная таблица (student_courses(student_id, course_id) с составным первичным ключом).
  • Нормализация: каждый факт хранится в одном месте. Не храните город клиента в каждом заказе — при переезде придётся обновлять все строки (аномалия обновления). Нормальные формы: 1НФ — атомарные значения, без списков в ячейке; 2НФ — нет зависимости от части составного ключа; 3НФ — нет зависимости неключевых полей друг от друга.
  • Денормализация — осознанное дублирование ради скорости чтения (счётчики, кеши отчётов).

PostgreSQL против SQLite

SQLite PostgreSQL
Запуск файл, без сервера отдельный сервер
Параллельная запись один писатель за раз много одновременных транзакций (MVCC)
Типы гибкие (TEXT, INTEGER, REAL, BLOB) строгие, богатые: timestamptz, jsonb, uuid, массивы
Где тесты, мобильные и десктопные приложения, прототипы веб-сервисы в продакшене

Из Python к PostgreSQL подключаются драйвером psycopg (или asyncpg), интерфейс похож на sqlite3 (DB-API), только параметры пишутся %s. Чаще — через ORM, о нём следующий урок.

Вопросы на собеседовании

  • Виды JOIN и чем LEFT отличается от INNER.
  • WHERE против HAVING; порядок выполнения запроса.
  • Что такое индекс, как он устроен, когда не помогает; составной индекс.
  • ACID, уровни изоляции, что такое грязное и неповторяемое чтение.
  • Как защититься от SQL-инъекций?
  • Задачи: второй по величине заказ, топ-3 в каждой категории (оконные функции), дубликаты (GROUP BY … HAVING count(*) > 1), клиенты без заказов (LEFT JOIN … IS NULL или NOT EXISTS).

Итог

  • SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT; NULL сравнивается через IS.
  • JOIN соединяет таблицы по ключам; LEFT JOIN оставляет строки без пары.
  • Оконные функции — агрегаты без схлопывания строк.
  • Индексы ускоряют чтение ценой записи; проверяйте план.
  • Транзакции через with conn:; значения — только параметрами.

Практика решено 0 из 7

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Схема с ограничениями. Напишите create_schema(conn), создающую две таблицы:

  • users: id — первичный ключ; email — текст, обязательный и уникальный; age — целое, проверка age >= 0;
  • orders: id — первичный ключ; user_id — обязательный внешний ключ на users(id); amount — обязательное число больше нуля.

Внешние ключи в SQLite выключены по умолчанию — включите их: PRAGMA foreign_keys = ON.

Какие тесты проверяют код (2)
import sqlite3
conn = sqlite3.connect(":memory:"); create_schema(conn)
conn.execute("INSERT INTO users (email, age) VALUES ('a@x.ru', 30)")
conn.execute("INSERT INTO orders (user_id, amount) VALUES (1, 99.5)")
assert conn.execute("SELECT count(*) FROM orders").fetchone() == (1,)

import sqlite3
conn = sqlite3.connect(":memory:"); create_schema(conn)
conn.execute("INSERT INTO users (email, age) VALUES ('a@x.ru', 30)")
bad = [
    "INSERT INTO users (email, age) VALUES ('a@x.ru', 1)",
    "INSERT INTO users (email, age) VALUES (NULL, 1)",
    "INSERT INTO users (email, age) VALUES ('b@x.ru', -5)",
    "INSERT INTO orders (user_id, amount) VALUES (42, 10)",
    "INSERT INTO orders (user_id, amount) VALUES (1, 0)",
    "INSERT INTO orders (user_id, amount) VALUES (NULL, 10)",
]
for sql in bad:
    try:
        conn.execute(sql)
    except sqlite3.IntegrityError:
        pass
    else:
        raise AssertionError(f"Запрос должен нарушать ограничение: {sql}")

Задание 2 Программа на Python

Выручка по городам. В базе есть таблицы customers(id, name, city) и orders(id, customer_id, amount). Напишите revenue_by_city(conn), возвращающую список кортежей (city, total, orders_count) по всем городам, где есть клиенты (без заказов — 0 и 0), отсортированный по total по убыванию, а при равенстве — по названию города. Всё посчитайте одним SQL-запросом (LEFT JOIN, GROUP BY, COALESCE).

Какие тесты проверяют код (2)
import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("""
    CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, city TEXT);
    CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER);
    INSERT INTO customers VALUES (1, 'Аня', 'Казань'), (2, 'Борис', 'Москва'), (3, 'Вера', 'Москва'),
                                 (4, 'Гоша', 'Омск'), (5, 'Даша', 'Тула');
    INSERT INTO orders VALUES (1, 1, 500), (2, 2, 300), (3, 2, 200), (4, 3, 700), (5, 5, 500);
""")
assert revenue_by_city(conn) == [("Москва", 1200, 3), ("Казань", 500, 1), ("Тула", 500, 1), ("Омск", 0, 0)]

import inspect, re; src = inspect.getsource(revenue_by_city); assert re.search(r"group\s+by", src, re.I) and "sorted" not in src and "for " not in src, "Посчитайте всё в SQL: GROUP BY и ORDER BY, без циклов и sorted в Python"
Задание 3 Программа на Python

Без SQL-инъекций. Функция ищет пользователя по логину, подставляя строку прямо в SQL. Перепишите её с параметрами (?), чтобы ввод ' OR '1'='1 ничего не находил. Возвращайте список кортежей (id, login).

Какие тесты проверяют код (2)
import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("CREATE TABLE users (id INTEGER PRIMARY KEY, login TEXT); INSERT INTO users (login) VALUES ('admin'), ('o''brien');")
assert find_user(conn, "admin") == [(1, "admin")]
assert find_user(conn, "o'brien") == [(2, "o'brien")], "Логин с апострофом тоже должен работать"
assert find_user(conn, "' OR '1'='1") == [], "Инъекция вернула всех пользователей"

import inspect; src = inspect.getsource(find_user); assert "?" in src and "{" not in src and "%" not in src, "Используйте параметры ?, а не форматирование строк"
Задание 4 Программа на Python

Перевод денег в транзакции. Таблица accounts(id, balance). Напишите transfer(conn, src, dst, amount): списывает amount со счёта src и зачисляет на dst атомарно. Если на src не хватает денег или любого из счетов нет — бросьте ValueError, и балансы не должны измениться. Используйте with conn: — при исключении внутри блока изменения откатятся.

Какие тесты проверяют код (2)
import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER); INSERT INTO accounts VALUES (1, 100), (2, 50);")
transfer(conn, 1, 2, 30)
assert conn.execute("SELECT balance FROM accounts ORDER BY id").fetchall() == [(70,), (80,)]

import sqlite3
conn = sqlite3.connect(":memory:")
conn.executescript("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER); INSERT INTO accounts VALUES (1, 100), (2, 50);")
for args in ((1, 2, 500), (1, 99, 10), (99, 1, 10)):
    try:
        transfer(conn, *args)
    except ValueError:
        pass
    else:
        raise AssertionError(f"transfer{args} должен бросить ValueError")
    assert conn.execute("SELECT balance FROM accounts ORDER BY id").fetchall() == [(100,), (50,)], f"После transfer{args} балансы изменились"

Задание 5 Заполнить таблицу

Какие строки попадут в результат? Таблицы: customers (есть клиенты без заказов) и orders (есть заказы с удалённым клиентом).

INNER JOINLEFT JOIN (customers слева)FULL OUTER JOIN
Клиент с заказами
Клиент без заказов
Заказ без клиента
Отметьте ячейку — «+», пустая — «−».
Задание 6 Один вариант ответа

Запрос SELECT * FROM orders WHERE customer_id = ? медленный на таблице в 10 млн строк. Что поможет в первую очередь?

Задание 7 Ввод ответа

Каким ключевым словом в SQL фильтруют группы после GROUP BY (в отличие от WHERE, который фильтрует строки до группировки)?

Войдите, чтобы отмечать пройденные уроки.