Чтобы учить SQL не на выдуманных табличках employees из трёх строк, нам нужна осмысленная база с настоящими связями и объёмом. Дальше весь курс - SELECT, JOIN, агрегаты, оконные функции, indexes и explain analyze - мы будем отрабатывать на одной и той же демобазе Авиаперевозки от Postgres Professional. В этом уроке разберём, из каких таблиц она состоит, как они связаны между собой, как её скачать и развернуть командой psql -f, и почему именно такая модель данных удобна для обучения postgresql.

Как это работает
Демобаза моделирует работу авиакомпании: пассажиры покупают билеты, билеты включают перелёты по конкретным рейсам, на рейсы назначаются самолёты с местами, а перед вылетом пассажир получает посадочный талон. Все таблицы лежат в отдельной схеме bookings, а не в public - это сделано специально, чтобы вы привыкали указывать схему и не путали учебные данные со своими.
Сердце модели - три уровня бронирования. Сначала идёт bookings: одно бронирование - это один поход в кассу или на сайт, у него есть номер book_ref из шести символов и общая сумма. Внутри бронирования может быть несколько билетов tickets - например, вы покупаете билеты сразу себе и ребёнку одним заказом. А каждый билет - это не один полёт, а связка перелётов ticket_flights: если летите с пересадкой Москва - Сочи через Ростов, то на один билет приходится два перелёта, у каждого своя цена и класс обслуживания.
Дальше идёт справочная часть, описывающая физический мир. flights - это расписание рейсов: номер рейса, плановые и фактические времена вылета и прилёта, аэропорты отправления и назначения, статус и какой самолёт назначен. airports хранит коды и города аэропортов, aircrafts - модели самолётов, а seats - схему салона, то есть какие места есть в конкретной модели и к какому классу они относятся.
Замыкает всё boarding_passes - посадочные талоны. Талон выдаётся не на билет вообще, а на конкретный перелёт, поэтому он ссылается сразу на пару ticket_no плюс flight_id и добавляет номер по порядку посадки и назначенное место. Так в модели честно отражается, что место в самолёте вам дают только при регистрации на конкретный рейс, а не при покупке.
Важная деталь актуальная на 2026: в текущих версиях демобазы airports и aircrafts - это не таблицы, а представления (view) над таблицами airports_data и aircrafts_data. Сделано это ради многоязычных названий: внутри они хранятся в типе jsonb, а представление вытаскивает строку на нужном языке. Поэтому когда вы делаете \d airports, не пугайтесь пометки view - данные настоящие, просто читаются через слой совместимости.
SQL и примеры
Сначала развернём базу. Скачиваете дамп с сайта Postgres Professional (есть варианты demo small, medium и big - отличаются глубиной истории полётов), затем подаёте файл в psql. Дамп сам создаёт базу demo, схему bookings и наполняет таблицы.
Код: Выделить всё
# в обычном терминале, не в psql
psql -f demo-small-20170815.sql postgres
Код: Выделить всё
psql -d demo
-- список таблиц учебной схемы
\dt bookings.*
-- структура таблицы рейсов: колонки, типы, ключи
\d bookings.flights
Код: Выделить всё
SET search_path = bookings, public;
Код: Выделить всё
SELECT
(SELECT count(*) FROM bookings) AS bookings,
(SELECT count(*) FROM tickets) AS tickets,
(SELECT count(*) FROM ticket_flights) AS ticket_flights,
(SELECT count(*) FROM flights) AS flights;
Код: Выделить всё
SELECT b.book_ref,
t.passenger_name,
f.flight_no,
f.scheduled_departure,
tf.fare_conditions,
tf.amount
FROM bookings b
JOIN tickets t ON t.book_ref = b.book_ref
JOIN ticket_flights tf ON tf.ticket_no = t.ticket_no
JOIN flights f ON f.flight_id = tf.flight_id
LIMIT 10;
Частые грабли
- Забыли схему. После загрузки таблицы лежат в bookings, а вы по привычке пишете SELECT * FROM tickets из public и получаете relation does not exist. Решение - SET search_path или полное имя bookings.tickets.
- Путают flight_no и flight_id. flight_no (например, PG0405) - это рейс из расписания, он повторяется каждый день. flight_id - уникальный конкретный вылет в конкретную дату. JOIN всегда делаем по flight_id.
- Ждут, что airports - таблица, и пытаются туда вставить строку. Это представление над airports_data; прямой INSERT упадёт или потребует учитывать jsonb-структуру.
- Запускают psql -f, уже находясь внутри psql. Команда psql -f - это команда операционной системы, её набирают в обычном терминале, а не в приглашении postgres=#.
- Берут самый большой дамп demo-big на слабой машине и удивляются долгой загрузке. Для учёбы хватает demo-small.
- Думают, что boarding_passes есть у всех билетов. Талоны существуют только для уже прошедшей регистрации, поэтому LEFT JOIN тут даёт NULL для будущих рейсов - это нормально.
- Скачайте demo-small с сайта Postgres Professional и разверните: psql -f demo-small-*.sql postgres.
- Подключитесь: psql -d demo, выполните \dt bookings.* и убедитесь, что видите все восемь таблиц.
- Выставьте SET search_path = bookings, public и проверьте \d seats - найдите, какой составной первичный ключ у таблицы мест.
- Посчитайте число рейсов по статусам: SELECT status, count(*) FROM flights GROUP BY status.
- Найдите модели самолётов и их дальность: SELECT model, range FROM aircrafts ORDER BY range DESC.
- Через цепочку JOIN выведите имена пассажиров и номера рейсов для одного выбранного book_ref.
- Выполните \d+ bookings.flights и посмотрите, какие внешние ключи (Foreign-key constraints) ссылаются на airports и aircrafts.
- Почему один билет (tickets) может быть связан с несколькими строками в ticket_flights?
- По каким ключам соединяются bookings, tickets, ticket_flights и flights?
- Чем flight_id отличается от flight_no и почему JOIN делают по flight_id?
- Почему airports и aircrafts в современной демобазе являются представлениями, а не таблицами?
- На что именно ссылается boarding_passes - на билет или на конкретный перелёт, и как это отражает реальную регистрацию?
- Зачем учебные таблицы вынесены в схему bookings, а не в public?