JOIN в SQL: соединяем таблицы INNER и LEFT JOIN
Данные живут в разных таблицах — JOIN соединяет их по ключу: INNER оставляет только пары, LEFT бережёт и одиночек.
Редакция Питоники
Всё это время в нашей базе книг author_id был числом-загадкой: 1, 2, 3 — но кто эти люди? Настоящие базы хранят каждую сущность в своей таблице: книги в одной, авторы в другой, а связывает их колонка-ссылка. И главный инструмент, который сшивает таблицы обратно, — JOIN. Это тема, на которой «понял» и «не понял» SQL расходятся навсегда, поэтому разберём её неспешно, на живой базе, где у одной книги автора намеренно нет.
Зачем данные разбивают на таблицы
Сначала — зачем вообще плодить таблицы. Можно было бы писать имя автора прямо в каждой книге: «Михаил Булгаков» трижды, у каждой книги своё поле. А теперь автор изменил имя в паспорте — правь три строки и молись, чтобы ни одной не пропустить. Одна опечатка — и «Михаил Булгаков» и «М. Булгаков» для базы станут двумя разными людьми, а отчёт раздробит его книги.
Правильная схема выглядит иначе: авторы живут в таблице authors, у каждого — свой id (помнишь PRIMARY KEY из урока о схеме), а в таблице книг хранится только этот номер — внешний ключ. Имя записано один раз, меняется одним UPDATE-ом, а книги ссылаются на него по номеру. Такой принцип — каждый факт хранится в одном месте — называют нормализацией.
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, и запрос остаётся читабельным.
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.
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(колонка) из прошлого урока? Здесь она взрывается по-настоящему.
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.
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.
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()
Что дальше
Ты прошёл самый серьёзный рубеж курса: умеешь соединять таблицы, различать 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)
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?
В редакторе готова база: таблица authors с четырьмя авторами (у Николая Гоголя книг нет) и таблица books. Выведи каждого автора и количество его книг — включая авторов без книг (ноль). Отсортируй по количеству по убыванию; при равенстве — по имени автора по алфавиту.
Что делает 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
sqlite3 · Урок 6
Агрегаты и GROUP BY в SQL: COUNT, SUM, AVG и HAVING
Считаем, суммируем и усредняем по всей таблице и по группам: COUNT, SUM, AVG, GROUP BY и фильтр групп HAVING.
FastAPI · Урок 7
Подключаем базу данных SQLite к FastAPI
Словарь задач из урока 5 умирает при перезапуске. Ставим на его место SQLite через SQLAlchemy: движок, сессии, Depends и CRUD, который переживает uvicorn --reload.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Pandas · Урок 7
Объединение таблиц: merge, concat и join в Pandas
merge с параметром how как SQL JOIN, concat для склейки выгрузок и join по индексу — соединяем товары, заказы и клиентов в одну таблицу и не теряем строки по дороге.
объединение таблиц pythonmerge pandas как объединить таблицы
BeautifulSoup / Scrapy · Урок 5
Парсинг таблиц и списков: собираем данные в структуру
Таблица — самая частая структура в вебе: курсы валют, расписания, прайсы. Собираем thead и tbody в список словарей, чистим цены и разбираем colspan.
парсинг таблиц pythonпарсинг таблиц с сайта python
sqlite3 · Урок 10
Проект: база книжной библиотеки на SQLite с отчётами SQL
Финальный проект курса: база библиотеки из трёх связанных таблиц — авторы, книги, выдачи — и четыре настоящих отчёта на SQL. Собираем всё, что выучили за девять уроков.
sqlite python проектбаза данных книг sqlite