ОП.08 Базы данных • Занятие 31 • Операторы манипулирования данными
1 / 15

Тема 5. Организация запросов SQL — Операторы манипулирования данными

1. Место DML в архитектуре реляционной СУБД

Язык манипулирования данными (Data Manipulation Language) управляет фактическим состоянием кортежей таблиц в рамках транзакционной модели ACID.

Ключевые операторы DML
  • INSERT: Добавление новых кортежей в кучу (heap) и синхронная регистрация ключей во всех индексах.
  • UPDATE: Модификация существующих записей в модели MVCC (создание новой версии кортежа).
  • DELETE: Пометка кортежа мертвым (dead tuple) через установку системного поля xmax.
Системные зависимости
  • Журнал WAL (Write-Ahead Log): предварительная фиксация изменений на диске перед записью в файлы таблиц.
  • Индексы B-Tree: каждая вставка и модификация не-HOT обновляет индексные деревья.
  • Уровни изоляции: защита от грязного чтения, неповторяемого чтения и фантомов.

2. Оператор INSERT INTO: Одиночная и пакетная вставка

Явное перечисление колонок защищает код от изменений схемы. Пакетная вставка кратно снижает накладные расходы.

-- Пакетная вставка (Batch Multi-row INSERT) 5 позиций оборудования
INSERT INTO warehouse_inventory (sku, product_name, quantity, unit_price)
VALUES 
  ('SKU-101', 'Процессор Intel Core i5-13400', 15, 18200.00),
  ('SKU-102', 'Материнская плата B760 DDR5', 20, 12500.00),
  ('SKU-103', 'Модуль памяти DDR5 32GB Kit', 35, 9800.00),
  ('SKU-104', 'Блок питания 750W Gold', 18, 8100.00),
  ('SKU-105', 'Корпус ATX Mesh Black', 12, 5400.00)
RETURNING id, sku, created_at;

Эффект батчинга: 1 сетевой пакет вместо N; 1 операция сброса WAL (fsync) вместо N; 1 проход оптимизатора запросов.

3. Вставка результатов выборки (INSERT INTO ... SELECT)

Позволяет производить миграцию и трансформацию массивов данных исключительно силами ядра СУБД.

-- Перенос закрытых кредитных договоров в исторический архив
INSERT INTO contracts_archive (
  contract_id, client_id, close_date, final_balance
)
SELECT 
  c.id, c.client_id, c.closed_at, c.balance
FROM active_contracts c
WHERE c.status = 'CLOSED' 
  AND c.closed_at < CURRENT_DATE - INTERVAL '1 year';

Преимущество архитектуры: Отсутствие передачи гигабайт сырых строк на сторону бэкенда через TCP-сокет. Операция выполняется in-memory на уровне движка СУБД с максимальной скоростью дискового ввода-вывода.

4. Клауза RETURNING: Атомарный возврат данных

Конструкция RETURNING возвращает клиенту значения вставленных, обновленных или удаленных кортежей без повторного SELECT.

Применение в DML
  • INSERT ... RETURNING id: получение сгенерированного суррогатного ключа IDENTITY / SERIAL.
  • UPDATE ... RETURNING: фиксация вычисленных значений (например, баланса после списания).
  • DELETE ... RETURNING: передача удаленных строк в систему аудита или брокер сообщений.
Пример запроса
INSERT INTO orders (client_id, amount)
VALUES (42, 15990.00)
RETURNING 
  order_id, 
  created_at, 
  status;

Исключает состояние гонки при попытке прочитать MAX(id) или currval() в многопоточной среде.

5. Проблема коллизий и Race Condition при вставке

Классический антипаттерн «проверить существование в SELECT, а затем выполнить INSERT» приводит к коллизиям в продакшне.

Антипаттерн приложения
-- Поток А: Проверяет наличие товара
SELECT id FROM warehouse WHERE sku = 'SKU-101'; -- (Пусто!)
-- Поток Б: Параллельно проверяет тот же товар
SELECT id FROM warehouse WHERE sku = 'SKU-101'; -- (Тоже пусто!)
-- Поток А выполняет: INSERT INTO warehouse ... -- УСПЕШНО!
-- Поток Б выполняет: INSERT INTO warehouse ... -- ОШИБКА: unique_violation!

Вывод: Проверка на уровне приложения не атомарна. Требуется нативная поддержка со стороны ядра СУБД — механизм UPSERT.

6. Концепция UPSERT: Атомарная обработка конфликтов

Конструкция ON CONFLICT (target_column) перехватывает ошибку уникальности прямо в момент вставки.

Ветка DO NOTHING

Игнорирует конфликтующие кортежи. Ошибка подавляется, транзакция не прерывается.

INSERT INTO tags (tag_name)
VALUES ('postgresql'), ('sql')
ON CONFLICT (tag_name) 
DO NOTHING;

Идеально для импорта справочников и очередей задач.

Ветка DO UPDATE SET

При конфликте преобразует операцию в атомарный UPDATE существующей строки.

INSERT INTO metrics (host, cpu)
VALUES ('srv-1', 78.4)
ON CONFLICT (host) 
DO UPDATE SET 
  cpu = EXCLUDED.cpu;

Использует псевдотаблицу EXCLUDED.

7. Архитектура псевдотаблицы EXCLUDED

Виртуальная таблица EXCLUDED содержит кортеж, который предлагался к вставке.

-- Идемпотентный UPSERT: суммирование остатков на складе при повторной накладной
INSERT INTO warehouse_inventory (sku, product_name, quantity, unit_price)
VALUES ('SKU-101', 'Процессор Intel Core i5-13400', 10, 17900.00)
ON CONFLICT (sku) 
DO UPDATE SET 
  -- склад.остаток (текущий) + EXCLUDED.остаток (новый приход)
  quantity = warehouse_inventory.quantity + EXCLUDED.quantity,
  unit_price = EXCLUDED.unit_price,
  updated_at = CURRENT_TIMESTAMP
RETURNING warehouse_inventory.id, warehouse_inventory.sku, warehouse_inventory.quantity;

Формула слияния: Имя таблицы обращается к старому сохраненному значению, префикс EXCLUDED. — к новому поступившему значению.

8. Оператор UPDATE: Модификация по предикату

Оператор UPDATE обновляет существующие кортежи. Забытая секция WHERE приводит к катастрофическим последствиям.

Безопасный синтаксис
-- Селективное обновление
UPDATE credit_contracts
SET 
  status = 'DELINQUENT',
  penalty_rate = 0.05
WHERE overdue_days > 30 
  AND status = 'ACTIVE'
RETURNING id, client_id;
Правило DBA
  • Перед запуском ручного UPDATE проверить выборку: SELECT COUNT(*) FROM ... WHERE ...
  • Любые ручные модификации на проде открывать через транзакцию: BEGIN; ... ROLLBACK;
  • Использовать RETURNING для контроля затронутых строк.

9. Конструкция UPDATE ... FROM (Join-модификация)

Специфика PostgreSQL: объединение обновляемой таблицы с другими таблицами через секцию FROM.

-- Обновление статуса приема врача по внешнему соединению с таблицей Оплаты
UPDATE doctor_appointments AS a
SET 
  status = 'CONFIRMED',
  updated_at = CURRENT_TIMESTAMP
FROM payment_invoices AS p
WHERE a.id = p.appointment_id
  AND p.payment_status = 'PAID'
  AND a.status = 'PENDING_PAYMENT'
RETURNING a.id, a.patient_id, a.status;

Критическое правило: Обновляемая таблица (doctor_appointments) не указывается в списке FROM повторно, иначе будет сформировано декартово произведение (Cartesian Product).

10. Оператор DELETE: Селективное удаление и аудит

Удаление кортежей с возвратом удаленных строк для дальнейшей обработки.

-- Селективное удаление отмененных приемов старше 1 года
DELETE FROM doctor_appointments
WHERE status = 'CANCELLED' 
  AND appointment_date < CURRENT_DATE - INTERVAL '1 year'
RETURNING id, patient_fio, appointment_date, cancellation_reason;

Физическая сущность DELETE в PostgreSQL: Строки не затираются нулями на диске. В заголовке кортежа выставляется поле xmax = tx_id. Кортеж перестает быть виден последующим транзакциям, превращаясь в Dead Tuple. Очистка выполняется VACUUM.

11. Физическое сравнение: DELETE против TRUNCATE

Параметр DELETE FROM t TRUNCATE TABLE t
Тип операции DML (уровень строк) DDL (уровень структуры)
Уровень блокировки ROW EXCLUSIVE ACCESS EXCLUSIVE (полный лок)
Генерация WAL Запись каждой строки Только факт выделения relfilenode
Возврат памяти ОС Нет (нужен VACUUM FULL) Мгновенно освобождает диск
Скорость на 1 млн строк Секунды (сканирование + WAL) Миллисекунды (пересоздание файла)

12. Внутренняя архитектура: DML и журнал WAL

Журнал предзаписи WAL гарантирует сохранность данных (Crash Recovery), но создает нагрузку на дисковый ввод-вывод.

Механизм WAL

  • Изменения буферизуются в памяти (shared buffers).
  • При COMMIT происходит системный вызов fsync() журнала WAL.
  • Массовые неупорядоченные DML-запросы вызывают переполнение WAL и задержки checkpoint.

Рекомендации HighLoad

  • Пакетная модификация порциями по 1 000 – 5 000 строк.
  • Использование временных таблиц UNLOGGED TABLE для промежуточных расчетов (не пишут в WAL).

13. Влияние на B-Tree индексы и оптимизация HOT

Каждый дополнительный индекс на таблице ускоряет чтение, но замедляет DML-операции.

Обычный UPDATE

Создается новая строка. Если колонка индексирована — во ВСЕ B-Tree индексы добавляются новые ссылки. Индексы разбухают (bloat).

Оптимизация HOT (Heap-Only Tuples)

Если индексированные поля не менялись и на текущей странице есть место, СУБД строит цепочку указателей внутри страницы. B-Tree индексы не трогаются! Колоссальная экономия IOPS.

14. Безопасность DML: Цена небрежности при формировании запросов

Little Bobby Tables (xkcd #327)
Тематический интернет-мем: «Exploits of a Mom» (культовый веб-комикс про «Little Bobby Tables», xkcd №327).
Автор: Randall Munroe. Первоисточник: xkcd.com/327. Лицензия: Creative Commons Attribution-NonCommercial 2.5 Generic (CC BY-NC 2.5).
Связь с темой занятия: Мама назвала сына Robert'); DROP TABLE Students;--. Склейка неэкранированных строковых переменных в операторах INSERT, UPDATE и DELETE открывает критическую уязвимость внедрения SQL-инъекций (SQL Injection). В промышленной разработке манипулирование данными должно выполняться строго через параметризованные запросы (Prepared Statements) и механизмы безопасного экранирования.