Не верь на слово, проверь под давлением.
Не верь на слово, проверь под давлением.

Задача

Исследовать и определить наиболее эффективный SQL-запрос, позволяющий получить информацию для анализа:

  • Неполных бронирований

  • Билетов без привязки к рейсам

  • Рейсов без процедуры посадки

  • Статистики по незавершённым операциям

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

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

Конфигурационные параметры СУБД
track_io_timing = 'on'
listen_addresses = '0.0.0.0'
logging_collector = 'on'
log_directory = '/log/pg_log'
log_destination = 'stderr'
log_rotation_size = '0'
log_rotation_age = '1d'
log_filename = 'postgresql-%u.log'
log_line_prefix = '%m| %d| %a| %u| %h| %p| %e| '
log_truncate_on_rotation = 'on'
log_checkpoints = 'on'
archive_mode = 'on'
archive_command = 'true'
archive_timeout = '30min'
checkpoint_timeout = '15min'
checkpoint_warning = '60'
checkpoint_completion_target = '0.9'
min_wal_size = '2GB'
max_wal_size = '8GB'
synchronous_commit = 'on'
wal_compression = 'on'
random_page_cost = '1.1'
effective_io_concurrency = '300'
wal_sender_timeout = '0'
autovacuum_naptime = '1s'
autovacuum_vacuum_scale_factor = '0.01'
autovacuum_analyze_scale_factor = '0.005'
autovacuum_vacuum_cost_delay = '2ms'
autovacuum_max_workers = '4'
autovacuum_work_mem = '256MB'
vacuum_cost_limit = '4000'
bgwriter_delay = '10ms'
bgwriter_lru_multiplier = '4'
bgwriter_lru_maxpages = '400'
max_locks_per_transaction = '256'
max_pred_locks_per_transaction = '256'
shared_buffers = '1919MB'
effective_cache_size = '5757MB'
work_mem = '14MB'
temp_buffers = '14MB'
maintenance_work_mem = '479MB'
max_worker_processes = '8'
max_parallel_workers = '8'
max_parallel_workers_per_gather = '4'
idle_in_transaction_session_timeout = '1h'
statement_timeout = '8h'
pg_stat_statements.track_utility = 'off'
max_parallel_maintenance_workers = '4'
hash_mem_multiplier = '2'
autovacuum_vacuum_insert_scale_factor = '0.01'
shared_preload_libraries = 'pg_stat_statements , pg_wait_sampling'
commit_delay = '1000'
log_autovacuum_min_duration = '0'
wipe_file_on_delete = 'on'
wipe_heaptuple_on_delete = 'on'
wipe_mem_on_free = 'on'
wipe_memctx_on_free = 'on'
wipe_xlog_on_free = 'on'
log_connections = 'on'
log_disconnections = 'on'
pg_stat_statements.track = 'all'
max_connections = '1000'

План нагрузочного тестирования (param.conf)

# НАСТРОЙКИ НАГРУЗОЧНОГО ТЕСТИРОВАНИЯ
# Максимальная нагрузка
finish_load = 20
# Тестовая БД
testdb = demo
# Веса сценариев
scenario1 = 1.0

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

Варианты SQL-запроса

LEFT JOIN
WITH random_period AS (
    SELECT 
        start_date,
        start_date + INTERVAL '30 days' AS end_date
    FROM (
        SELECT 
            book_date AS start_date
        FROM bookings
        WHERE book_date <= (SELECT MAX(book_date) FROM bookings) - INTERVAL '30 days'
        ORDER BY RANDOM()
        LIMIT 1
    ) AS random_date
)
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,
    rp.start_date AS period_start,
    rp.end_date AS period_end
FROM random_period rp
CROSS JOIN LATERAL (
    SELECT *
    FROM bookings b
    WHERE b.book_date BETWEEN rp.start_date AND rp.end_date
) b
LEFT JOIN tickets t ON b.book_ref = t.book_ref
LEFT JOIN segments s ON t.ticket_no = s.ticket_no
LEFT JOIN flights f ON s.flight_id = f.flight_id
LEFT JOIN boarding_passes bp ON 
    s.ticket_no = bp.ticket_no 
    AND s.flight_id = bp.flight_id
ORDER BY b.book_date, t.ticket_no;
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.book_ref = t.book_ref
LEFT JOIN segments s ON t.ticket_no = s.ticket_no
LEFT JOIN flights f ON s.flight_id = f.flight_id
LEFT JOIN boarding_passes bp ON s.ticket_no = bp.ticket_no AND s.flight_id = bp.flight_id
WHERE EXISTS (
    SELECT 1 FROM random_period rp 
    WHERE b.book_date BETWEEN rp.start_date AND rp.end_date
)
ORDER BY b.book_date, t.ticket_no;
TUNING(оптимизация структуры запроса)
WITH random_period AS (
    SELECT 
        book_date AS start_date,
        book_date + INTERVAL '30 days' AS end_date
    FROM bookings 
    WHERE book_date <= (SELECT MAX(book_date) FROM bookings) - INTERVAL '30 days'
    ORDER BY RANDOM() 
    LIMIT 1
),
filtered_bookings AS (
    SELECT book_ref, book_date
    FROM bookings
    WHERE book_date BETWEEN (SELECT start_date FROM random_period) 
                        AND (SELECT end_date FROM random_period)
)
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 filtered_bookings b
LEFT JOIN LATERAL (
    SELECT ticket_no, passenger_name
    FROM tickets 
    WHERE book_ref = b.book_ref
) t ON true
LEFT JOIN LATERAL (
    SELECT flight_id, fare_conditions, price
    FROM segments 
    WHERE ticket_no = t.ticket_no
) s ON true
LEFT JOIN LATERAL (
    SELECT status, scheduled_departure, actual_departure, route_no
    FROM flights 
    WHERE flight_id = s.flight_id
) f ON true
LEFT JOIN LATERAL (
    SELECT seat_no, boarding_no
    FROM boarding_passes 
    WHERE ticket_no = t.ticket_no AND flight_id = s.flight_id
) bp ON true
ORDER BY b.book_date, t.ticket_no;
ARRAY(Частичная агрегация)
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
),
booking_summary AS (
    SELECT 
        b.book_ref,
        b.book_date,
        COUNT(t.ticket_no) as ticket_count,
        ARRAY_AGG(t.ticket_no) as ticket_numbers
    FROM bookings b
    LEFT JOIN tickets t ON b.book_ref = t.book_ref
    WHERE b.book_date BETWEEN (SELECT start_date FROM random_period) 
                        AND (SELECT end_date FROM random_period)
    GROUP BY b.book_ref, b.book_date
)
SELECT 
    bs.book_ref,
    bs.book_date,
    bs.ticket_count,
    tn.ticket_no,  -- Используем развернутое значение
    t.passenger_name,
    s.flight_id,
    f.status
FROM booking_summary bs
CROSS JOIN UNNEST(bs.ticket_numbers) AS tn(ticket_no)  -- Разворачиваем массив здесь
LEFT JOIN tickets t ON t.ticket_no = tn.ticket_no      -- Теперь соединяем по одиночному значению
LEFT JOIN segments s ON s.ticket_no = t.ticket_no
LEFT JOIN flights f ON f.flight_id = s.flight_id
ORDER BY bs.book_date, tn.ticket_no;  -- Сортируем по развернутому значению
TEMPORARY TABLE (Временная таблица)
-- Создаем временную таблицу для отфильтрованных бронирований
CREATE TEMPORARY TABLE temp_filtered_bookings AS
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.*
FROM bookings b
CROSS JOIN random_period rp
WHERE b.book_date BETWEEN rp.start_date AND rp.end_date;

-- Создаем индексы на временной таблице
CREATE INDEX tmp_idx_book_ref ON temp_filtered_bookings (book_ref);
CREATE INDEX tmp_idx_book_date ON temp_filtered_bookings (book_date);

--TEST-5 "TEMP TABLES"
EXPLAIN (ANALYZE , SUMMARY , COSTS)
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 temp_filtered_bookings b
LEFT JOIN tickets t ON b.book_ref = t.book_ref
LEFT JOIN segments s ON t.ticket_no = s.ticket_no
LEFT JOIN flights f ON s.flight_id = f.flight_id
LEFT JOIN boarding_passes bp ON s.ticket_no = bp.ticket_no AND s.flight_id = bp.flight_id
ORDER BY b.book_date, t.ticket_no;
WHERE (Изменение условия WHERE)
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
CROSS JOIN random_period rp
LEFT JOIN tickets t ON b.book_ref = t.book_ref
LEFT JOIN segments s ON t.ticket_no = s.ticket_no
LEFT JOIN flights f ON s.flight_id = f.flight_id
LEFT JOIN boarding_passes bp ON s.ticket_no = bp.ticket_no AND s.flight_id = bp.flight_id
WHERE b.book_date BETWEEN rp.start_date AND rp.end_date
  AND (f.status IS NULL OR f.status IN ('Scheduled', 'On Time', 'Departed')) -- Фильтр по статусу
ORDER BY b.book_date, t.ticket_no;

Дополнение

Для варианта "TEMPORARY TABLE (Временная таблица)" - временная таблица temp_filtered_bookings пересоздается перед выполнением тестового запроса.

Эксперименты

Вариант-1: Использование EXISTS

Вариант-2: Оптимизация структуры запроса

Вариант-3: Частичная агрегация

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

Вариант-5: Условие WHERE

Производительность СУБД в ходе нагрузочного тестирования

График изменения операционной скорости в ходе нагрузочного тестирования
График изменения операционной скорости в ходе нагрузочного тестирования

Характерные особенности производительности СУБД, для нагрузки 15 соединений и выше

1. Катастрофическое падение производительности тестового запроса "WHERE"

  • Резкое снижение: Значения колеблются (12-16), затем происходит обвал до 4, после 18 соединений — до 2, после 22 соединения производительность полностью деградирует.

  • Важность: Падение до 0 означает, что эти запросы фактически перестают выполняться.

2. Стабилизация и рост производительности "JOIN"

  • Положительный тренд: После 15 соединений "JOIN" показывает стабильные значения 14, а после 18 соединений производительность возрастает до 16.

3. Улучшение "EXISTS"

  • Рост производительности: С 18 соединений значение увеличивается с 12 до 14 и остается стабильным.

4. Стабильность "TUNING"

  • Консистентность: После 15 соединений производительность стабильно держится на уровне 22.

5. "ARRAY"- максимальная стабильность

  • Идеальная стабильность: После 12 соединений производительность достигает своего максимального стабильного значения 32.

6. "TEMP TABLE" высокая производительность

  • Высокий уровень: Значения 214-216 (самые высокие абсолютные значения).

Критические выводы:

  1. Наиболее уязвимый компонент: "WHERE"- сильная деградация под нагрузкой.

  2. Наиболее устойчивые: "ARRAY" и "TUNING" запросы показывают отличную стабильность.

  3. Парадокс: В то время как "WHERE" деградируют до 0, другие типы запросов либо стабильны, либо улучшаются. Это может указывать на:
    Конкуренцию за ресурсы
    Проблемы специфические для условий "WHERE"

Метрики производительности инфраструктуры, для нагрузки 15 соединений и выше

1. WA (Ожидание ввода-вывода) - CPU Wait I/O

  • JOIN: высокий уровень (11-16%), особенно в конце (до 17%)

  • EXISTS: умеренный (12-15%), относительно стабильный

  • TUNING: самый высокий (24-28%) - явная проблема с дисковыми операциями

  • ARRAY: минимальный (0-3%) - низкая нагрузка на I/O

  • TEMPORARY TABLE: минимальный (0-1%)

  • WHERE: низкий (1-3%)

2. BI (Blocks In - блоки ввода)

  • JOIN: высокий (66K-86K)

  • EXISTS: умеренный (68K-86K)

  • TUNING: высокий (69K-73K)

  • ARRAY: средний (32K-35K)

  • TEMPORARY TABLE: средний (62K-63K)

  • WHERE: экстремально высокий (200K-250K) - интенсивное чтение с диска

3. US (User CPU - пользовательский процессор)

  • JOIN: умеренный (54-58%)

  • EXISTS: умеренный (56-64%)

  • TUNING: высокий (59-66%)

  • ARRAY: очень высокий (94-95%)

  • TEMPORARY TABLE: очень высокий (93-95%)

  • WHERE: высокий (80%)

4. R (Run Queue - очередь процессов)

  • JOIN: 7-9 процессов

  • EXISTS: 7-10 процессов

  • TUNING: 7-8 процессов

  • ARRAY: 11-15 процессов

  • TEMPORARY TABLE: 13-15 процессов

  • WHERE: 12-15 процессов

5. CACHE (Кэш памяти)

  • JOIN: стабильно высокий (6800-7100)

  • EXISTS: стабильно высокий (6800-7000)

  • TUNING: стабильно высокий (6700-6900)

  • ARRAY: снижается (6000-6300)

  • TEMPORARY TABLE: умеренный (6400-6600)

  • WHERE: умеренный (6100-6300)

6. SY (System CPU - системный процессор)

  • JOIN: низкий (5%)

  • EXISTS: низкий (5%)

  • TUNING: низкий (5%)

  • ARRAY: низкий (4%)

  • TEMPORARY TABLE: низкий (5%)

  • WHERE: высокий (12-14%) - интенсивные системные вызовы

7. SI (Swap In)

Данные по SI отсутствуют .

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

  1. Наиболее проблемный запрос: "TUNING" - имеет максимальное время ожидания I/O (24-28%) при умеренной нагрузке CPU.

  2. Наиболее CPU-интенсивные запросы: "ARRAY" и "TEMPORARY TABLE" - достигают 95% использования user CPU.

  3. Наиболее I/O-интенсивный запрос: "WHERE" - экстремально высокий BI (до 250K) и повышенное системное время CPU (14%).

  4. Наибольшая очередь выполнения: "ARRAY", "TEMPORARY TABLE" и "WHERE" (11-15 процессов).

  5. Наименее нагруженные запросы: "JOIN" и "EXISTS" показывают сбалансированны�� метрики без экстремальных значений.

Итог

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

  1. Для данной СУБД и в условиях высокой конкурентной нагрузки, для обеспечения максимальной производительности - рекомендован вариант "ТEMPORARY TABLE"

  2. Наихудший вариант - "WHERE"

Производительность и стабильность инфраструктуры

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

  2. Наихудший вариант - "WHERE"

P.S. Следующий цикл публикация будет посвящен способам пагинации.