Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 7 из 10 Средний 45 мин 140 XP

JOIN в SQL: соединяем таблицы INNER и LEFT JOIN

Данные живут в разных таблицах — JOIN соединяет их по ключу: INNER оставляет только пары, LEFT бережёт и одиночек.

Редакция Питоники

Всё это время в нашей базе книг author_id был числом-загадкой: 1, 2, 3 — но кто эти люди? Настоящие базы хранят каждую сущность в своей таблице: книги в одной, авторы в другой, а связывает их колонка-ссылка. И главный инструмент, который сшивает таблицы обратно, — JOIN. Это тема, на которой «понял» и «не понял» SQL расходятся навсегда, поэтому разберём её неспешно, на живой базе, где у одной книги автора намеренно нет.

Зачем данные разбивают на таблицы

Сначала — зачем вообще плодить таблицы. Можно было бы писать имя автора прямо в каждой книге: «Михаил Булгаков» трижды, у каждой книги своё поле. А теперь автор изменил имя в паспорте — правь три строки и молись, чтобы ни одной не пропустить. Одна опечатка — и «Михаил Булгаков» и «М. Булгаков» для базы станут двумя разными людьми, а отчёт раздробит его книги.

Правильная схема выглядит иначе: авторы живут в таблице authors, у каждого — свой id (помнишь PRIMARY KEY из урока о схеме), а в таблице книг хранится только этот номер — внешний ключ. Имя записано один раз, меняется одним UPDATE-ом, а книги ссылаются на него по номеру. Такой принцип — каждый факт хранится в одном месте — называют нормализацией.

две таблицы: authors и books
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, genre TEXT, price INTEGER, pages INTEGER)"
)

authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин"), (3, "Лев Толстой"), (4, "Николай Гоголь")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)

books = [
    (1, "Мастер и Маргарита", 1, "роман", 850, 480), (2, "Собачье сердце", 1, "повесть", 420, 160),
    (3, "Евгений Онегин", 2, "поэма", 500, 320), (4, "Руслан и Людмила", 2, "поэма", 380, 210),
    (5, "Война и мир", 3, "роман", 1200, 960), (6, "Анна Каренина", 3, "роман", 950, 864),
    (7, "Русские народные сказки", None, "сказки", 300, 240), (8, "Белая гвардия", 1, "роман", 700, 416),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books)

print("authors:")
for row in cur.execute("SELECT * FROM authors"):
    print(row)
print("books:")
for row in cur.execute("SELECT title, author_id FROM books"):
    print(row)
Вывод
authors:
(1, 'Михаил Булгаков')
(2, 'Александр Пушкин')
(3, 'Лев Толстой')
(4, 'Николай Гоголь')
books:
('Мастер и Маргарита', 1)
('Собачье сердце', 1)
('Евгений Онегин', 2)
('Руслан и Людмила', 2)
('Война и мир', 3)
('Анна Каренина', 3)
('Русские народные сказки', None)
('Белая гвардия', 1)

Схема готова, но попробуй ответить на простой вопрос: чья это книга «Собачье сердце»? В выводе только безликая единица. Расшифровку надо искать во второй таблице — глазами или... правильно, поручить это базе.

INNER JOIN: соединяем по ключу

INNER JOIN приписывает к каждой строке первой таблицы строки второй, где совпало условие ON. В запросе таблицам дают короткие псевдонимы — алиасы: books b и authors a, дальше колонки пишутся как b.title и a.name, и запрос остаётся читабельным.

INNER JOIN: книги с именами авторов
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, genre TEXT, price INTEGER, pages INTEGER)"
)
authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин"), (3, "Лев Толстой"), (4, "Николай Гоголь")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1, "роман", 850, 480), (2, "Собачье сердце", 1, "повесть", 420, 160),
    (3, "Евгений Онегин", 2, "поэма", 500, 320), (4, "Руслан и Людмила", 2, "поэма", 380, 210),
    (5, "Война и мир", 3, "роман", 1200, 960), (6, "Анна Каренина", 3, "роман", 950, 864),
    (7, "Русские народные сказки", None, "сказки", 300, 240), (8, "Белая гвардия", 1, "роман", 700, 416),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books)

for row in cur.execute(
    "SELECT b.title, a.name FROM books b "
    "INNER JOIN authors a ON b.author_id = a.id"
):
    print(row)
Вывод
('Мастер и Маргарита', 'Михаил Булгаков')
('Собачье сердце', 'Михаил Булгаков')
('Евгений Онегин', 'Александр Пушкин')
('Руслан и Людмила', 'Александр Пушкин')
('Война и мир', 'Лев Толстой')
('Анна Каренина', 'Лев Толстой')
('Белая гвардия', 'Михаил Булгаков')

Механика проста: база берёт строку книги, читает её author_id, ищет в authors строку с таким id и склеивает их. Пройдись по выводу: Булгаков встретился трижды — у него три книги, Гоголь не встретился вовсе. И главная пропажа: сказок нет. У них author_id — NULL, условие NULL = 4 не выполняется никогда, пары нет — и INNER JOIN молча выбросил строку. Иногда это желаемое поведение («показать только оформленные связи»), но чаще это сюрприз: восемь книг на входе — семь строк на выходе.

Что вернёт LEFT JOIN без пары?

LEFT JOIN делает то же самое, но с одной оговоркой: он сохраняет ВСЕ строки левой таблицы — той, что стоит в FROM. Где пара нашлась, к строке приклеиваются колонки правой таблицы. Где не нашлась — строка всё равно останется, а вместо недостающих значений встанет NULL.

LEFT JOIN: те же таблицы, другой результат
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, genre TEXT, price INTEGER, pages INTEGER)"
)
authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин"), (3, "Лев Толстой"), (4, "Николай Гоголь")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1, "роман", 850, 480), (2, "Собачье сердце", 1, "повесть", 420, 160),
    (3, "Евгений Онегин", 2, "поэма", 500, 320), (4, "Руслан и Людмила", 2, "поэма", 380, 210),
    (5, "Война и мир", 3, "роман", 1200, 960), (6, "Анна Каренина", 3, "роман", 950, 864),
    (7, "Русские народные сказки", None, "сказки", 300, 240), (8, "Белая гвардия", 1, "роман", 700, 416),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books)

for row in cur.execute(
    "SELECT b.title, a.name FROM books b "
    "LEFT JOIN authors a ON b.author_id = a.id"
):
    print(row)
Вывод
('Мастер и Маргарита', 'Михаил Булгаков')
('Собачье сердце', 'Михаил Булгаков')
('Евгений Онегин', 'Александр Пушкин')
('Руслан и Людмила', 'Александр Пушкин')
('Война и мир', 'Лев Толстой')
('Анна Каренина', 'Лев Толстой')
('Русские народные сказки', None)
('Белая гвардия', 'Михаил Булгаков')

LEFT JOIN сохраняет все строки левой таблицы, а там, где пары не нашлось, подставляет NULL вместо недостающих значений. Те же два запроса, тот же ON — и восемь строк вместо семи: сказки выжили, а на месте автора стоит None. Это и есть ответ на вопрос заголовка: строка левой таблицы с NULL в колонках правой. Поисковик «книг-сирот» строится на этом бесплатно: оставить только те строки, где пара не нашлась, — значит потребовать NULL у ключа правой таблицы.

книги без автора
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, genre TEXT, price INTEGER, pages INTEGER)"
)
authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин"), (3, "Лев Толстой"), (4, "Николай Гоголь")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1, "роман", 850, 480), (2, "Собачье сердце", 1, "повесть", 420, 160),
    (3, "Евгений Онегин", 2, "поэма", 500, 320), (4, "Руслан и Людмила", 2, "поэма", 380, 210),
    (5, "Война и мир", 3, "роман", 1200, 960), (6, "Анна Каренина", 3, "роман", 950, 864),
    (7, "Русские народные сказки", None, "сказки", 300, 240), (8, "Белая гвардия", 1, "роман", 700, 416),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books)

for row in cur.execute(
    "SELECT b.title FROM books b "
    "LEFT JOIN authors a ON b.author_id = a.id "
    "WHERE a.id IS NULL"
):
    print(row)
Вывод
('Русские народные сказки',)

Пара LEFT JOIN + WHERE a.id IS NULL — стандартный паттерн проверки данных: заказы без клиента, товары без категории, сотрудники без отдела. Важно писать именно IS NULL, а не = NULL: сравнение с NULL через равно всегда даёт «неизвестно» — ты уже ловил эту ловушку в уроке про WHERE и IS NULL.

COUNT с JOIN: сколько книг у каждого автора

Теперь сила комбинации из урока про GROUP BY: сгруппируем соединённые таблицы и посчитаем книги каждого автора. Обрати внимание на агрегат: мы считаем COUNT(b.id) — непустые идентификаторы книг.

книги каждого автора, включая нулевых
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, genre TEXT, price INTEGER, pages INTEGER)"
)
authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин"), (3, "Лев Толстой"), (4, "Николай Гоголь")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1, "роман", 850, 480), (2, "Собачье сердце", 1, "повесть", 420, 160),
    (3, "Евгений Онегин", 2, "поэма", 500, 320), (4, "Руслан и Людмила", 2, "поэма", 380, 210),
    (5, "Война и мир", 3, "роман", 1200, 960), (6, "Анна Каренина", 3, "роман", 950, 864),
    (7, "Русские народные сказки", None, "сказки", 300, 240), (8, "Белая гвардия", 1, "роман", 700, 416),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books)

for row in cur.execute(
    "SELECT a.name, COUNT(b.id) AS cnt FROM authors a "
    "LEFT JOIN books b ON b.author_id = a.id "
    "GROUP BY a.name ORDER BY cnt DESC, a.name"
):
    print(row)
Вывод
('Михаил Булгаков', 3)
('Александр Пушкин', 2)
('Лев Толстой', 2)
('Николай Гоголь', 0)

Мы шли от авторов — поэтому каждый автор попал в отчёт, даже Гоголь, у которого книг нет: LEFT JOIN сохранил его строку, а COUNT(b.id) посчитал NULL-ы честным нулём. Помнишь разницу между COUNT(*) и COUNT(колонка) из прошлого урока? Здесь она взрывается по-настоящему.

ловушка: COUNT со звёздочкой после LEFT JOIN
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, genre TEXT, price INTEGER, pages INTEGER)"
)
authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин"), (3, "Лев Толстой"), (4, "Николай Гоголь")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1, "роман", 850, 480), (2, "Собачье сердце", 1, "повесть", 420, 160),
    (3, "Евгений Онегин", 2, "поэма", 500, 320), (4, "Руслан и Людмила", 2, "поэма", 380, 210),
    (5, "Война и мир", 3, "роман", 1200, 960), (6, "Анна Каренина", 3, "роман", 950, 864),
    (7, "Русские народные сказки", None, "сказки", 300, 240), (8, "Белая гвардия", 1, "роман", 700, 416),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books)

for row in cur.execute(
    "SELECT a.name, COUNT(*) AS cnt FROM authors a "
    "LEFT JOIN books b ON b.author_id = a.id "
    "GROUP BY a.name ORDER BY cnt DESC, a.name"
):
    print(row)
Вывод
('Михаил Булгаков', 3)
('Александр Пушкин', 2)
('Лев Толстой', 2)
('Николай Гоголь', 1)
демонстрация: дубль в справочнике
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, genre TEXT, price INTEGER, pages INTEGER)"
)
books = [
    (1, "Мастер и Маргарита", 1, "роман", 850, 480), (2, "Собачье сердце", 1, "повесть", 420, 160),
    (3, "Евгений Онегин", 2, "поэма", 500, 320), (4, "Руслан и Людмила", 2, "поэма", 380, 210),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books)

# справочник БЕЗ PRIMARY KEY: Пушкин записан дважды
cur.execute("CREATE TABLE authors_bad (id INTEGER, name TEXT)")
authors_bad = [(1, "Михаил Булгаков"), (2, "Александр Пушкин"), (2, "Александр Пушкин")]
cur.executemany("INSERT INTO authors_bad VALUES (?, ?)", authors_bad)

total = 0
for row in cur.execute(
    "SELECT b.title, a.name FROM books b "
    "JOIN authors_bad a ON b.author_id = a.id"
):
    print(row)
    total += 1
print("строк:", total)
Вывод
('Мастер и Маргарита', 'Михаил Булгаков')
('Собачье сердце', 'Михаил Булгаков')
('Евгений Онегин', 'Александр Пушкин')
('Евгений Онегин', 'Александр Пушкин')
('Руслан и Людмила', 'Александр Пушкин')
('Руслан и Людмила', 'Александр Пушкин')
строк: 6

Каждая пушкинская книга нашла двух «Пушкиных» и задублировалась. Именно поэтому в настоящих схемах ключ защищает PRIMARY KEY и UNIQUE — ограничения из урока о схеме, которые не пустят дубль в справочник.

Реальный пример: выручка по авторам через два JOIN

Последний рывок — запрос уровня настоящего проекта. Добавим таблицу продаж sales: какая книга и сколько экземпляров продано. Вопрос бизнеса: выручка по авторам. Цепочка рассуждений: продажи ссылаются на книги через book_id, книги — на авторов через author_id; значит, два JOIN подряд, а дальше — знакомый GROUP BY с SUM.

два JOIN и агрегат: выручка по авторам
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, genre TEXT, price INTEGER, pages INTEGER)"
)
cur.execute("CREATE TABLE sales (book_id INTEGER, qty INTEGER)")

authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин"), (3, "Лев Толстой"), (4, "Николай Гоголь")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1, "роман", 850, 480), (2, "Собачье сердце", 1, "повесть", 420, 160),
    (3, "Евгений Онегин", 2, "поэма", 500, 320), (4, "Руслан и Людмила", 2, "поэма", 380, 210),
    (5, "Война и мир", 3, "роман", 1200, 960), (6, "Анна Каренина", 3, "роман", 950, 864),
    (7, "Русские народные сказки", None, "сказки", 300, 240), (8, "Белая гвардия", 1, "роман", 700, 416),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books)
sales = [(1, 12), (2, 30), (3, 7), (5, 4), (6, 9)]
cur.executemany("INSERT INTO sales VALUES (?, ?)", sales)

for row in cur.execute(
    "SELECT a.name, SUM(s.qty * b.price) AS revenue FROM sales s "
    "JOIN books b ON s.book_id = b.id "
    "JOIN authors a ON b.author_id = a.id "
    "GROUP BY a.name ORDER BY revenue DESC"
):
    print(row)
Вывод
('Михаил Булгаков', 22800)
('Лев Толстой', 13350)
('Александр Пушкин', 3500)

Разбери цепочку: каждая продажа склеилась со своей книгой (первый JOIN), каждая книга — со своим автором (второй), GROUP BY собрал стопки по авторам, SUM умножил тираж на цену и сложил. Здесь я намеренно написал JOIN без INNER — это то же самое: INNER в SQL можно не писать. И снова заметь пропажу: Гоголя в отчёте нет — продаж не было, и цепочка INNER-ов его отрезала. Хочешь видеть всех авторов, включая нулевые строки, — первым ставь authors LEFT JOIN books, а revenue оборачивай в обработку NULL. Запомни это место: на собеседованиях «почему в отчёте не все клиенты» в девяти случаях из десяти — вот этот вопрос.

В песочнице база каждый раз собирается заново — и это идеально для экспериментов. В настоящем приложении база живёт в файле, и запросы выглядят абсолютно так же: меняется только путь в connect.

тот же JOIN, но база в файле
import sqlite3

conn = sqlite3.connect("library.db")   # файл рядом со скриптом
cur = conn.cursor()

for row in cur.execute(
    "SELECT b.title, a.name FROM books b "
    "LEFT JOIN authors a ON b.author_id = a.id "
    "ORDER BY b.title"
):
    print(row)

conn.close()
Песочница не работает с файлами, поэтому блок локальный: запусти его в проекте с готовой базой library.db — или собери базу сам по блокам выше. После conn.close() данные живут между запусками: в этом главное отличие файловой базы от нашей in-memory песочницы. С серверными базами вроде PostgreSQL запрос будет тем же — изменится способ подключения.

Что дальше

Ты прошёл самый серьёзный рубеж курса: умеешь соединять таблицы, различать INNER и LEFT, считать по группам и не наступать на дубли. Дальше — безопасные UPDATE и DELETE: менять данные так, чтобы не снести чужие строки. А когда пройдёшь раздел до конца, загляни в урок про SQLite в FastAPI: там эти же запросы встают внутрь настоящего API — навык сразу находит работу.

INNER JOIN оставляет только пары, LEFT JOIN — всех слева, а COUNT(b.id) вместо звёздочки спасает отчёт от фантомных единиц.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author_id INTEGER)"
)
authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1),
    (2, "Собачье сердце", 1),
    (3, "Евгений Онегин", 2),
    (4, "Русские народные сказки", None),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?)", books)

cur.execute("SELECT COUNT(*) FROM books b JOIN authors a ON b.author_id = a.id")
print(cur.fetchone())
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author_id INTEGER)"
)
authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1),
    (2, "Собачье сердце", 1),
    (3, "Евгений Онегин", 2),
    (4, "Русские народные сказки", None),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?)", books)

for row in cur.execute(
    "SELECT b.title FROM books b "
    "LEFT JOIN authors a ON b.author_id = a.id "
    "WHERE a.id IS NULL"
):
    print(row[0])
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute(
    "CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, "
    "author_id INTEGER, price INTEGER)"
)
cur.execute("CREATE TABLE sales (book_id INTEGER, qty INTEGER)")
authors = [(1, "Михаил Булгаков"), (2, "Александр Пушкин")]
cur.executemany("INSERT INTO authors VALUES (?, ?)", authors)
books = [
    (1, "Мастер и Маргарита", 1, 850),
    (2, "Собачье сердце", 1, 420),
    (3, "Евгений Онегин", 2, 500),
]
cur.executemany("INSERT INTO books VALUES (?, ?, ?, ?)", books)
sales = [(1, 10), (2, 5)]
cur.executemany("INSERT INTO sales VALUES (?, ?)", sales)

for row in cur.execute(
    "SELECT a.name, SUM(s.qty * b.price) AS revenue FROM sales s "
    "JOIN books b ON s.book_id = b.id "
    "JOIN authors a ON b.author_id = a.id "
    "GROUP BY a.name"
):
    print(row)
Проверь себя
0 / 5

1. Какие строки оставляет INNER JOIN?

2. Что подставит LEFT JOIN в колонки правой таблицы, если пары нет?

3. В books 8 книг, у одной author_id = NULL. Сколько строк вернёт books LEFT JOIN authors?

4. Почему в отчёте «книги каждого автора» после LEFT JOIN считают COUNT(b.id), а не COUNT(*)?

5. Что делает алиас в записи FROM books b?

Карточки терминов
Запомнено: 0 / 6
Практика

В редакторе готова база: таблица authors с четырьмя авторами (у Николая Гоголя книг нет) и таблица books. Выведи каждого автора и количество его книг — включая авторов без книг (ноль). Отсортируй по количеству по убыванию; при равенстве — по имени автора по алфавиту.

practice.py
Вопросы и ответы по уроку

Что делает INNER JOIN в SQL?

INNER JOIN соединяет строки двух таблиц по условию ON и оставляет только пары: SELECT b.title, a.name FROM books b INNER JOIN authors a ON b.author_id = a.id — к каждой книге приклеится строка её автора. Строки без пары (NULL-ключ или отсутствие совпадения) в результат не попадают: из восьми книг с одним автором-NULL останется семь.

Что вернёт LEFT JOIN, если пары нет?

Строку левой таблицы с NULL вместо колонок правой. Это делает LEFT JOIN инструментом проверки данных: связка LEFT JOIN + WHERE a.id IS NULL находит книги без автора, заказы без клиента, сотрудников без отдела. Только сравнивай через IS NULL, а не через = NULL.

Чем LEFT JOIN отличается от INNER JOIN простыми словами?

INNER — только совпавшие пары, LEFT — все строки левой таблицы, даже без пары. На одной и той же базе INNER дал семь строк, LEFT — восемь. Практическое правило: главный справочник (авторы, клиенты) ставь слева и соединяй LEFT, если нужны все его записи; INNER — когда нужны только состоявшиеся связи.

Зачем выносить авторов в отдельную таблицу?

Чтобы каждый факт хранился один раз: имя автора записано в authors единожды, а в книгах — только его id. Меняется имя — меняется одна строка, а не сотни книг; опечатка не способна раздробить одного автора на двух. Это нормализация — принцип, на котором стоят все реляционные базы, и JOIN — способ сшить нормализованные таблицы обратно для отчёта.

Понравился урок? Сошлитесь на него

«LEFT JOIN сохраняет все строки левой таблицы, а там, где пары не нашлось, подставляет NULL вместо недостающих значений.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.