DML и индексы
Все примеры в лабораторной выполняются на учебной базе demo, развёрнутой в лабораторной работе 3. Подключитесь к базе и установите схему поиска
\c demo
SET search_path TO bookings;Убедитесь, что таблицы схемы bookings доступны
\dtРанее в работе мы ограничивались выборкой данных. Сейчас уже будут применяться операции, изменяющие содержимое таблиц. Чтобы сохранить учебную схему в исходном виде — все эксперименты с INSERT, UPDATE, DELETE лучше выполнять на временных копиях постоянных таблиц.
Временная таблица создаётся командой CREATE TEMP TABLE и автоматически удаляется при закрытии сеанса. Если копия повреждена — достаточно переподключиться и создать её заново. Если же изменена постоянная таблица (например, командой без WHERE) и требуется восстановить исходное состояние — базу можно пересоздать из дампа demo_small.sql.
Версия демонстрационной базы. Все запросы в работе написаны для редакции 2017 года (
demo_small.sql), как и в лабораторной 5. Начиная с 1 сентября 2025 года ее обновили, ознакомиться с актуальной версией можно на сайте Postgres Pro.
Подготовка рабочей таблицы
Для экспериментов создадим копию aircrafts, над которой можно выполнять любые INSERT, UPDATE и DELETE без риска испортить данные. Конструкция WITH NO DATA копирует только структуру таблицы, без строк
CREATE TEMP TABLE aircrafts_tmp AS
SELECT * FROM aircrafts WITH NO DATA;
ALTER TABLE aircrafts_tmp ADD PRIMARY KEY (aircraft_code);
ALTER TABLE aircrafts_tmp ADD UNIQUE (model)Ограничения целостности (PRIMARY KEY, UNIQUE) при копировании структуры не переносятся, поэтому накладываются отдельно командой ALTER TABLE
Проверка созданной таблицы
\d aircrafts_tmpВставка строк: INSERT
С INSERT мы уже познакомились в лабораторной работе №3, здесь рассмотрим ситуации, которые уже чуть больше приближены к реальным практическим вариантам использования.
INSERT … SELECT. Заполнить таблицу данными из другой таблицы проще всего подзапросом, без ручного перечисления строк
INSERT INTO aircrafts_tmp
SELECT * FROM aircraftsПосле выполнения команды PostgreSQL сообщит о количестве вставленных строк (INSERT 0 9). Первое число — исторический OID строки, в современных версиях PostgreSQL (12 и новее) он всегда равен 0, поскольку поддержка таблиц WITH OIDS удалена. Второе число — количество затронутых строк. Проверка результата
SELECT * FROM aircrafts_tmp ORDER BY modelКоличество атрибутов и их типы во вставляемых строках должны быть согласованы с целевой таблицей. При явном перечислении столбцов (INSERT INTO t (a, b, c)) подзапрос допускает меньше столбцов, чем есть в таблице — недостающие получат значения по умолчанию или NULL
Предложение RETURNING. Любая команда INSERT, UPDATE или DELETE в PostgreSQL способна возвращать строки, над которыми она отработала. В конец команды добавляется RETURNING с перечислением нужных столбцов или *
INSERT INTO aircrafts_tmp
VALUES ('IL9', 'Ilyushin IL96', 9800)
RETURNING *Команда выведет добавленную строку. Это удобно для проверки результата и особенно полезно при генерации первичного ключа СУБД — RETURNING сразу возвращает сгенерированное значение без повторного SELECT
Предложение
RETURNING— расширение PostgreSQL, не входящее в стандарт SQL
Разрешение конфликтов. При вставке строки возможно нарушение ограничения первичного или уникального ключа. По умолчанию PostgreSQL прерывает команду с ошибкой. Для пропуска такой строки без ошибки используется предложение ON CONFLICT DO NOTHING
INSERT INTO aircrafts_tmp
VALUES ('SU9', 'Sukhoi SuperJet-100', 3000)
ON CONFLICT DO NOTHINGКоманда вернёт INSERT 0 0 — ноль вставленных строк, без ошибки. Без указания конкретного ограничения проверка идёт по всем: первичному ключу и всем уникальным. Для явного указания столбца, по которому допустим конфликт, используется запись ON CONFLICT (aircraft_code)
INSERT INTO aircrafts_tmp
VALUES ('SU9', 'Sukhoi SuperJet-100', 3000)
ON CONFLICT (aircraft_code) DO NOTHING
RETURNING *Здесь проверяется конфликт только по aircraft_code. Если вставляемая строка конфликтует по другому ключу (например, уникальному model), команда всё равно завершится ошибкой, поскольку этот конфликт не заявлен в ON CONFLICT
INSERT ... ON CONFLICT— расширение PostgreSQL. В стандарте SQL аналогичную задачу решает командаMERGE(PostgreSQL поддерживает её с версии 15)
На любимиом нами сайте sql-ex ознакомтесь с информацией в разделе: Оператор INSERT. Упражнения — в отдельном DML-этапе sql-ex.ru. Доступ к нему открывается после прохождения обучающего (SELECT) этапа
Также советую ознакомиться с документацией в официальном разделе документации Postgres: PostgreSQL docs — INSERT, ON CONFLICT.
Задание
- Попробуйте вставить в
aircrafts_tmpстроку с уже существующимaircraft_code— получите сообщение об ошибке. Повторите команду сON CONFLICT (aircraft_code) DO NOTHINGи убедитесь, что ошибки больше нет - Добавьте в
aircrafts_tmpдве новые строки (например,Ilyushin IL96иIlyushin IL96-300) одной командойINSERT ... VALUES (...), (...) RETURNING *. Проверьте, что выводятся обе добавленные строки
Обновление строк: UPDATE
Команда UPDATE изменяет существующие строки таблицы. Его обязательная составляющая ключевое слово SET, задающее, какие столбцы и на какие значения обновить. А при помощи WHERE можно ограничить множество строк, к которым применяется обновление
UPDATE aircrafts_tmp
SET range = range * 1.2
WHERE model ~ '^Bom'Команда увеличивает на 20% дальность полёта всех самолётов, название которых начинается с «Bom» (регулярное выражение ^Bom). Оператор ~ — проверка на соответствие регулярному выражению POSIX (расширение PostgreSQL, в стандарте SQL соответствующий оператор — SIMILAR TO)
Как и у INSERT, в конец команды UPDATE можно добавить RETURNING, чтобы увидеть изменённые строки
UPDATE aircrafts_tmp
SET range = range * 1.2
WHERE model ~ '^Bom'
RETURNING *Обратите внимание! Самая частая и опасная ошибка при работе с
UPDATE— пропущенное предложениеWHERE. Без него команда обновит все строки таблицы. То же самое относится кDELETE. Безопасный приём — сперва выполнитьSELECTс тем жеWHEREи проверить, какие строки отбираются, а затем заменитьSELECTнаUPDATEилиDELETE
Задание
- В
aircrafts_tmpодной командой увеличьтеrangeна 10% для всех самолётов Airbus и верните изменённые строки черезRETURNING.
Удаление строк: DELETE
Команда DELETE удаляет строки таблицы. Как и у UPDATE, ключевая часть — предложение WHERE. Удаление из рабочей копии aircrafts_tmp моделей Bombardier
DELETE FROM aircrafts_tmp
WHERE model ~ '^Bom'Предложение RETURNING возвращает удалённые строки, что удобно для проверки правильности фильтра
DELETE FROM aircrafts_tmp
WHERE model ~ '^Bom'
RETURNING *TRUNCATE: быстрая очистка таблицы. Для удаления всех строк таблицы вместо DELETE FROM t без условий предпочтительна команда TRUNCATE
TRUNCATE aircrafts_tmpDELETE обходит таблицу построчно и для каждой строки записывает факт удаления в журнал операций. TRUNCATE сразу освобождает всё хранилище таблицы, записывая только одну операцию. На больших таблицах это даст существенный выигрыш в скорости выполнения операции. Однако, у TRUNCATE есть свои ограничения: невозможно сделать откат на уровне отдельной строки, команда не применяется к таблице, на которую ссылаются внешние ключи без CASCADE и не активирует построчные триггеры BEFORE DELETE и AFTER DELETE (Триггеры - это набор функций, который вызывается при совершении определнных действий над таблицей / набором данных).
Как обычно, предлагаю ознакомиться и попрактиковаться на сайте PostgresPro, раздел операторы UPDATE и DELETE. Упражнения — в DML-этапе вместе с задачами на
INSERTБолее подробная и расширенная документация на сайте PostgresPro: PostgreSQL docs — UPDATE, DELETE, TRUNCATE
Задание
- В
aircrafts_tmpодной командой удалите все модели Bombardier (model ~ '^Bom') с выводом удалённых строк черезRETURNING. Убедитесь, что количество строк в таблице уменьшилось на ожидаемое число
Индексы: общая информация
На малых таблицах SQL-команды выполняются практически мгновенно. На больших таблицах (миллионы и десятки миллионов строк) время выполнения запроса становится ощутимым и зависит от того, какие индексы построены на таблице
Индекс — вспомогательная структура данных, связанная с таблицей и построенная на основе её столбцов. Строки таблицы хранятся неупорядоченно, в порядке вставки, а записи индекса упорядочены. Каждая запись индекса содержит значение индексируемого столбца и указатель на соответствующую строку таблицы. При поиске СУБД обращается не к полному перебору таблицы, а к отсортированному индексу (поиск за логарифмическое время), после чего переходит к таблице по прямой ссылке
По умолчанию PostgreSQL создаёт индексы типа B-tree (сбалансированное дерево). Этот тип подходит для большинства типовых задач — поиск по равенству и диапазону, сортировка, соединения. Существуют и другие типы — Hash, GiST, SP-GiST, GIN, BRIN — применяемые в более узких случаях (полнотекстовый поиск, геоданные, массивы и т. п.). Мы остановимся только на наиболее простых и классических - B-tree
Индексы, создаваемые автоматически. При наложении на таблицу ограничения PRIMARY KEY или UNIQUE PostgreSQL автоматически создаёт уникальный индекс для поддержки этого ограничения — проверка дубликатов через индекс выполняется за логарифмическое время. Индексы таблицы boarding_passes
\d boarding_passesВ выводе присутствует раздел «Индексы»
Индексы:
"boarding_passes_pkey" PRIMARY KEY, btree (ticket_no, flight_id)
"boarding_passes_flight_id_boarding_no_key" UNIQUE CONSTRAINT, btree (flight_id, boarding_no)
"boarding_passes_flight_id_seat_no_key" UNIQUE CONSTRAINT, btree (flight_id, seat_no)Имена индексов, созданных автоматически, имеют узнаваемый вид: для первичного ключа — <таблица>_pkey, для уникального ключа — <таблица>_<столбцы>_key
Создание индекса вручную. Пользовательские индексы создаются командой CREATE INDEX
CREATE INDEX
ON airports (airport_name)Имя индекса необязательно — PostgreSQL сгенерирует его автоматически в виде <таблица>_<столбец>_idx. Список индексов таблицы
\d airportsВ списке появится строка "airports_airport_name_idx" btree (airport_name). Удаление индекса — команда DROP INDEX
DROP INDEX airports_airport_name_idxПолный список индексов текущей базы выводится командой утилиты psql
\diИзмерение эффекта индекса. Для оценки эффекта индекса на реальном запросе в psql включается встроенный секундомер
\timing onПосле каждой команды выводится время её выполнения. Точное имя пассажира зависит от редакции базы, поэтому сперва подбирается существующее имя, встречающееся достаточно часто
SELECT passenger_name
FROM tickets
GROUP BY passenger_name
HAVING count(*) > 50
LIMIT 1Полученное имя подставляется в запрос по столбцу passenger_name, по которому индекс ещё не построен (в примере ниже стоит 'IVAN IVANOV', в актуальной базе его нужно заменить на реально встречающееся значение)
SELECT count(*)
FROM tickets
WHERE passenger_name = 'IVAN IVANOV'Время выполнения фиксируется, после чего создаётся индекс по столбцу
CREATE INDEX tickets_passenger_name_idx
ON tickets (passenger_name)При повторении того же SELECT время сократится на порядок-другой. Вместо полного перебора таблицы tickets (в зависимости от редакции — сотни тысяч или миллионы строк) СУБД теперь ищет в упорядоченном индексе
Прим. Абсолютные значения времени на конкретной машине будут отличаться от приведённых в учебных материалах и зависят от оборудования, версии PostgreSQL и состояния кеша. Сравнивать имеет смысл относительные цифры — во сколько раз один запрос быстрее другого
Фактический способ выполнения запроса — полный перебор или поиск по индексу — показывает команда EXPLAIN, выводящая план выполнения
EXPLAIN
SELECT count(*) FROM tickets WHERE passenger_name = 'IVAN IVANOV'В плане будет либо строка Seq Scan on tickets (последовательный перебор), либо Index Scan using ... on tickets или Bitmap Index Scan (поиск по индексу). Команда EXPLAIN ANALYZE дополнительно выполняет запрос и показывает фактическое время
Созданный индекс удаляется
DROP INDEX tickets_passenger_name_idxЦена индекса. Индекс ускоряет чтение, но замедляет запись. При каждом INSERT, UPDATE (при изменении индексируемого столбца) и DELETE PostgreSQL поддерживает индекс в актуальном состоянии. Индекс также занимает место на диске, пропорциональное размеру таблицы. Поэтому индексы создаются прицельно — по столбцам, регулярно используемым в WHERE, ORDER BY или условиях соединения
Индекс малополезен при низкой селективности выборки. Если запрос извлекает 50% или больше строк таблицы, полный перебор работает не медленнее поиска по индексу с последующим обращением к половине строк. Максимальный эффект от индекса достигается при выборке малой доли строк — единицы процентов или меньше
Типы индексов
Индексы по нескольким столбцам. Индекс строится по одному столбцу или по нескольким сразу. В последнем случае записи индекса упорядочены сначала по первому столбцу, при совпадении — по второму и далее по списку (лексикографическая сортировка)
CREATE INDEX flights_dep_arr_idx
ON flights (departure_airport, arrival_airport)Такой индекс эффективен для запросов с условием по первому столбцу (WHERE departure_airport = 'SVO') и по обоим столбцам вместе (WHERE departure_airport = 'SVO' AND arrival_airport = 'LED'). Если запрос фильтрует только по второму столбцу (WHERE arrival_airport = 'LED' без условия на departure_airport), индекс по паре (departure_airport, arrival_airport) не поможет — планировщик выберет последовательный перебор. Отсюда вытекает правило: порядок столбцов в составном индексе важен
Уникальные индексы. Уникальный индекс гарантирует уникальность значений столбца (или комбинации столбцов) и одновременно ускоряет поиск по нему
CREATE UNIQUE INDEX aircrafts_model_uniq
ON aircrafts_tmp (model)Попытка вставки строки с дублирующим значением приводит к ошибке нарушения ограничения. Специфика NULL: в уникальном индексе NULL считается несовпадающим ни с одним другим значением, в том числе с другим NULL. Две строки с NULL в уникальном столбце не конфликтуют — если требуется обратное поведение, столбец объявляется NOT NULL
Прим. Создание уникального индекса — альтернатива ограничению
UNIQUEвCREATE TABLEилиALTER TABLE. Функционально они эквивалентны: заUNIQUEвнутри стоит тот же уникальный индекс
Документация с подробным. пояснением и примерами: PostgreSQL docs — Indexes, Multicolumn Indexes
Задание
- На таблице
ticket_flightsвыполнитеSELECT count(*) FROM ticket_flights WHERE fare_conditions = 'Business'несколько раз, замерив среднее время. Создайте индекс поfare_conditionsи повторите эксперимент.
Прим. После завершения экспериментов пользовательские индексы удаляются командой
DROP INDEX имя_индекса. Временные таблицы пропадают сами при отключении от базы, а созданные на постоянных таблицах индексы остаются и влияют на выполнение последующих работ. Список пользовательских индексов выводится командой\di
Дополнительно можно потренироваться (по желанию):
- DML-этап sql-ex.ru — задачи на
INSERT,UPDATE,DELETEиMERGEс автоматической проверкой. Выполняются на учебных базах sql-ex.ru (корабли, компьютерная фирма, переработка вторсырья), а не на схемеbookings - pgexercises.com — раздел Updates (INSERT, UPDATE, DELETE) на учебной базе PostgreSQL, в свободном доступе, без регистрации