Найти в Дзене
Postgres DBA

↗️1️⃣Оптимизация SQL-запроса с использованием pg_expecto: Часть-1 "EXISTS"

Не верь на слово, проверь под давлением. Предисловие Оптимизировать запрос в вакууме — просто. Но как он поведет себя, когда десятки таких же запросов одновременно борются за ресурсы? Методология исследования Тестовая среда и инструменты: СУБД: PostgreSQL 17 Инструмент нагрузочного тестирования: pg_expecto Тестовая база данных: "Демобаза 2.0" (большой размер, сложная схема) Условия тестирования: параллельная нагрузка, ресурсоемкий запрос Нагрузка на СУБД ℹ️Тестовый запрос - LEFT JOIN Этот запрос демонстрирует: LEFT JOIN bookings → tickets Все бронирования, даже без привязанных билетов LEFT JOIN tickets → segments Все билеты, даже без перелётов LEFT JOIN segments → flights Все перелёты, даже без информации о рейсах LEFT JOIN segments → boarding_passes Все перелёты, даже без посадочных талонов Особенности использования LEFT OUTER JOIN: Сохраняются все записи из левой таблицы Если в правой таблице нет совпадений, поля заполняются NULL Позволяет увидеть "неполные" данные (например, брон
Оглавление
Не верь на слово, проверь под давлением.
Не верь на слово, проверь под давлением.

Предисловие

Оптимизировать запрос в вакууме — просто. Но как он поведет себя, когда десятки таких же запросов одновременно борются за ресурсы?

Методология исследования

Тестовая среда и инструменты:

Нагрузка на СУБД

-2

ℹ️Тестовый запрос - LEFT JOIN

Тестовый запрос - LEFT OUTER JOIN WITH random_period AS (   SELECT    start_date,    start_date + INTERVAL '30 days' AS end_date   FROM (    SELECT    book_date AS start_date,    ROW_NUMBER() OVER (ORDER BY RANDOM()) AS rn    FROM bookings    WHERE book_date <= (SELECT MAX(book_date) FROM bookings) - INTERVAL '30 days'   ) AS dates   WHERE rn = 1 ) SELECT   b.book_ref,   b.book_date,   t.ticket…
Postgres DBA29 ноября

Этот запрос демонстрирует:

  1. LEFT JOIN bookings → tickets
  2. Все бронирования, даже без привязанных билетов
  3. LEFT JOIN tickets → segments
  4. Все билеты, даже без перелётов
  5. LEFT JOIN segments → flights
  6. Все перелёты, даже без информации о рейсах
  7. LEFT JOIN segments → boarding_passes
  8. Все перелёты, даже без посадочных талонов

Особенности использования LEFT OUTER JOIN:

  • Сохраняются все записи из левой таблицы
  • Если в правой таблице нет совпадений, поля заполняются NULL
  • Позволяет увидеть "неполные" данные (например, бронирования без билетов)
  • Показывает рейсы без выданных посадочных талонов

Такой запрос может быть полезен для анализа:

  • Неполных бронирований
  • Билетов без привязки к рейсам
  • Рейсов без процедуры посадки
  • Статистики по незавершённым операциям

CTE random_period:

  • Выбирает случайную дату из существующих бронирований
  • Гарантирует, что период полностью входит в доступный диапазон дат
  • Использует ROW_NUMBER() OVER (ORDER BY RANDOM()) для случайной выборки

Таблицы используемые в тестовом запросе

Таблицы используемые в тестовом запросе demo=# \d bookings    Unlogged table "bookings.bookings"   Column  |   Type   | Collation | Nullable | Default --------------+--------------------------+-----------+----------+---------  book_ref   | character(6)   |   | not null |  book_date  | timestamp with time zone |   | not null |  total_amount | numeric(10,2)   |   | not null | Indexes:   "bookings…
Postgres DBA29 ноября

План выполнения тестового запроса - LEFT JOIN

--План выполнения тестового запроса - LEFT JOIN  Sort (cost=2544355.52..2550077.32 rows=2288717 width=93) (actual time=58778.563..59050.999 rows=1146771 loops=1)   Sort Key: b.book_date, t.ticket_no   Sort Method: external merge Disk: 121832kB   CTE random_period    -> Subquery Scan on random_date (cost=66429.70..66429.71 rows=1 width=16) (actual time=2883.937..2883.945 rows=1 loops…
Postgres DBA29 ноября

cost=2544355.52..2550077.32

1️⃣Эксперимент-1 : Оптимизация с использованием EXISTS

Тестовый запрос - EXISTS

--Тестовый запрос - EXISTS WITH random_period AS (   SELECT    book_date AS start_date,    book_date + INTERVAL '30 days' AS end_date   FROM bookings   ORDER BY RANDOM()   LIMIT 1 ) SELECT   b.book_ref,   b.book_date,   t.ticket_no,   t.passenger_name,   s.flight_id,   f.status,   f.scheduled_departure,   f.actual_departure,   bp.seat_no,   bp.boarding_no FROM bookings b LEFT JOIN tickets t ON b…
Postgres DBA29 ноября

План выполнения тестового запроса - EXISTS

План выполнения тестового запроса - EXISTS  Sort (cost=2920577.58..2926299.37 rows=2288716 width=77) (actual time=52126.170..52366.389 rows=1076571 loops=1)   Sort Key: b.book_date, t.ticket_no   Sort Method: external merge Disk: 97592kB   CTE random_period    -> Limit (cost=191879.84..191879.84 rows=1 width=24) (actual time=4368.349..4368.353 rows=1 loops=1)    -> Sort (cost…
Postgres DBA29 ноября

cost=2920577.58..2926299.37

Сравнительный анализ планов выполнения запросов

Проведи сравнительный анализ планов выполнения тестовых запросов

Общие характеристики

Оба запроса выполняют одинаковую логику - выборку данных о бронированиях за случайный 30-дневный период с присоединением связанных данных о билетах, рейсах и посадочных талонах.

Ключевые различия

1. Стратегия соединения с random_period

LEFT JOIN запрос:

  • Использует CROSS JOIN LATERAL для соединения с отфильтрованными бронированиями
  • Эффективно использует индекс idx_bookings_book_date
  • Обрабатывает только 397,632 строк из bookings

EXISTS запрос:

  • Использует EXISTS с подзапросом в WHERE
  • Выполняет полное сканирование таблицы bookings (7,113,192 строк)
  • Применяет Join Filter который отбрасывает 6,721,707 строк

2. Производительность CTE random_period

LEFT JOIN: 2,883ms

  • Использует индексный сканирование с условием даты
  • Сортирует только подходящие записи (6,894,590 строк)

EXISTS: 4,368ms

  • Полное сканирование всей таблицы bookings
  • Сортирует все 7,113,192 записей

3. Время выполнения

LEFT JOIN: 59,151ms

  • Больше времени на сортировку (121MB на диск)
  • Более сложный план выполнения

EXISTS: 52,459ms

  • Быстрее на 6,692ms (≈11%)
  • Меньший объем сортировки (97MB на диск)

4. Использование индексов

LEFT JOIN: Лучше использует индексы

  • Индексное сканирование для фильтрации по дате
  • Эффективное вложенное соединение

EXISTS: Больше последовательных сканирований

  • Принуждает к полному сканированию bookings
  • Меньшая эффективность фильтрации

Сравнительный анализ производительности и ожиданий СУБД в ходе нагрузочного тестирования при использовании запроса "JOIN" и запроса "EXISTS"

График изменения операционной скорости в ходе нагрузочного тестирования при использоваении тестового запроса "JOIN" и "EXISTS"
График изменения операционной скорости в ходе нагрузочного тестирования при использоваении тестового запроса "JOIN" и "EXISTS"
График изменения относительной разницы операционной скорости для запроса "EXISTS" по сравнению с тестовым запросом "JOIN"
График изменения относительной разницы операционной скорости для запроса "EXISTS" по сравнению с тестовым запросом "JOIN"

⬇️Среднее уменьшение операционной скорости при использовании запроса "EXISTS" составило 12.84%

График изменения ожиданий СУБД в ходе нагрузочного тестирования при использоваении тестового запроса "JOIN" и "EXISTS"
График изменения ожиданий СУБД в ходе нагрузочного тестирования при использоваении тестового запроса "JOIN" и "EXISTS"
График изменения относительной разницы ожиданий СУБД для запроса "EXISTS" по сравнению с тестовым запросом "JOIN"
График изменения относительной разницы ожиданий СУБД для запроса "EXISTS" по сравнению с тестовым запросом "JOIN"

1. Ввод-вывод (IO)

JOIN: начальное ~6,800 → максимальное ~17,400 (рост в 2.6 раза)

EXISTS: начальное ~6,700 → максимальное ~16,100 (рост в 2.4 раза)

Вывод: Оба запроса создают значительную нагрузку на подсистему ввода-вывода, но JOIN требует на 8-10% больше операций IO.

2. Межпроцессное взаимодействие (IPC)

JOIN: начальное ~3,900 → максимальное ~26,500 (рост в 6.8 раза)

EXISTS: начальное ~3,900 → максимальное ~24,600 (рост в 6.2 раза)

Вывод: JOIN создает значительно большую нагрузку на IPC (на 15-20% выше), что указывает на более интенсивное взаимодействие между процессами.

3. Легковесные блокировки (LWLOCK)

JOIN: начальное ~13 → максимальное ~143 (рост в 11 раз)

EXISTS: начальное ~14 → максимальное ~133 (рост в 9.5 раз)

Вывод: JOIN требует значительно больше легковесных блокировок (на 20-25% выше), что может указывать на более сложную синхронизацию.

4. Таймауты (TIMEOUT)

JOIN: начальное ~4 → максимальное ~21 (рост в 5.3 раза)

EXISTS: начальное ~3 → максимальное ~17 (рост в 5.7 раза)

Вывод: Оба запроса демонстрируют сходное поведение по таймаутам, что может указывать на схожие проблемы с конкуренцией за ресурсы.

Сравнительный анализ метрик iostat для дискового устройства используемого файловой системой /data, в ходе нагрузочного тестирования при использовании запроса "JOIN" и запроса "EXISTS"

Проведи сравнительный анализ и сформируй итог по показаниям iostat для тестовых запросов в ходе нагрузочного тестирования

Оба теста демонстрируют идентичные показатели - все метрики остаются абсолютно стабильными на протяжении всего тестирования без каких-либо изменений.

Операции записи:

w/s: 0 - нулевые операции записи в секунду

wrqm/s: 0 - нулевые merged write requests

%wrqm: 49 - 49% запросов на запись объединяются

w_await: 2 ms - низкое время ожидания записи

wareq_sz: 10 - средний размер запроса записи

Утилизация диска:

%util: 0 - нулевая утилизация дискового устройства

Операции отмены/сброса:

f/s: 0 - нулевые операции flush/отмены

f_await: 0 - нулевое время ожидания flush операций

Ключевые выводы

1. Отсутствие дисковой нагрузки

  • Оба типа запросов полностью выполняются в памяти
  • Нулевая утилизация диска (%util = 0) указывает на отсутствие операций ввода-вывода
  • Данные полностью кэшированы в shared_buffers или оперативной памяти

2. Преимущества кэширования

  • Высокая эффективность кэша PostgreSQL
  • Оптимальная конфигурация shared_buffers
  • Полное отсутствие операций чтения с диска

3. Сравнительная характеристика JOIN vs EXISTS

Метрика JOIN EXISTS Вывод

w/s 0 0 Одинаково

wrqm/s 0 0 Одинаково

%wrqm 49 49 Одинаково

w_await 2 ms 2 ms Одинаково

%util 0 0 Одинаково

Текущее состояние - оптимальное:

-- Настройки работают эффективно

shared_buffers -- оптимально настроен

work_mem -- достаточный объем

effective_cache_size -- корректно настроен

Итоговый вывод

Оба запроса (JOIN и EXISTS) демонстрируют идеальную производительность с точки зрения подсистемы ввода-вывода:

  • Полное отсутствие дисковой нагрузки
  • Оптимальное использование кэша
  • Стабильные низкие задержки
  • Идентичная эффективность для обоих типов запросов

Текущая конфигурация PostgreSQL и оборудование оптимально справляются с нагрузкой.

Сравнительный анализ метрик vmstat в ходе нагрузочного тестирования при использовании запроса "JOIN" и запроса "EXISTS"

Проведи сравнительный анализ и сформируй итог по показаниям vmstat для тестовых запросов в ходе нагрузочного тестирования

Оба теста демонстрируют высокую нагрузку на систему, но с различными характеристиками распределения ресурсов.

1. Процессы (procs)

JOIN: procs_r: 4 → 9 (рост в 2.25 раза) | procs_b: 1 → 3

EXISTS: procs_r: 4 → 10 (рост в 2.5 раза) | procs_b: 1 → 2

Вывод: EXISTS создает больше процессов в состоянии выполнения, но меньше блокированных процессов.

2. Память (memory)

JOIN EXISTS

swpd: 203 → 217 217 → 212

free: 180 → 179 181 → 180

buff: 106 → 10 7 → 7

cache: 7012 → 6771 7129 → 6869

Вывод: Оба запроса эффективно используют кэш, но JOIN активнее использует буферы.

3. Ввод-вывод (I/O)

JOIN EXISTS

io_bi: 66077 → 86122 63848 → 77227

io_bo: 3474 → 8023 2276 → 4984

Вывод: JOIN создает на 15-20% большую нагрузку на ввод-вывод.

4. Системные события

JOIN EXISTS

system_in: 10449 → 14832 10030 → 14569

system_cs: 9681 → 14137 9967 → 13686

Вывод: JOIN вызывает больше системных прерываний и переключений контекста.

5. Использование CPU

JOIN EXISTS

cpu_us: 35 → 58 37 → 63

cpu_sy: 4 → 5 4 → 5

cpu_id: 47 → 14 46 → 14

cpu_wa: 10 → 12 11 → 12

Вывод: EXISTS потребляет больше пользовательского CPU времени.

Общие проблемы обоих запросов:

  1. Высокое использование CPU (idle снижается с ~46% до ~14%)
  2. Значительная нагрузка на I/O
  3. Увеличение количества процессов

Детальный анализ трендов

Фазы нагрузки:

  1. Начальная фаза (первые 20-30 измерений):
  2. Оба запроса показывают сходное поведение
  3. Стабильное использование ресурсов
  4. Пиковая фаза (середина теста):
  5. JOIN демонстрирует более резкий рост I/O нагрузки
  6. EXISTS показывает более плавное увеличение нагрузки
  7. Финальная фаза:
  8. Оба запроса стабилизируются на высоком уровне нагрузки
  9. JOIN сохраняет более высокие показатели I/O

EXISTS демонстрирует лучшую общую эффективность по ключевым метрикам:

  • На 15-20% меньше операций I/O
  • Меньше блокированных процессов
  • Более стабильное использование памяти
  • Меньше системных прерываний

JOIN показывает:

  • ⚠️ Более высокую нагрузку на I/O подсистему
  • ⚠️ Больше блокировок процессов
  • ⚠️ Более интенсивные системные прерывания

Общий итог : Часть-1 "EXISTS"

➡️Для условий высокой параллельной нагрузки и конкуренции за вычислительные ресурсы, с точки зрения производительности СУБД - использование конструкции EXISTS не дает существенных преимуществ.

Производительность СУБД:

⬇️Среднее уменьшение операционной скорости при использовании запроса "EXISTS" составило 12.84%

Использование ресурсов СУБД:

  • JOIN создает на 15-25% большую нагрузку на подсистему ввода-вывода и межпроцессное взаимодействие
  • EXISTS генерирует меньше блокировок и системных прерываний

Системные метрики:

  • EXISTS показывает более стабильное использование памяти и меньше блокированных процессов
  • JOIN создает более интенсивную нагрузку на I/O подсистему

Продолжение цикла "Сравнительный анализ методов оптимизации ресурсоёмкого SQL-запроса под конкурентной нагрузкой с использованием pg_expecto":

2️⃣Часть-2: Оптимизация структуры запроса

3️⃣Часть-3: Частичная агрегация

4️⃣Часть-4: Использование временных таблиц

5️⃣Часть-5: Оптимизация с помощью WHERE

6️⃣Итог: Сводный анализ