Оптимизация Python и PostgreSQL
Анализ планов выполнения запросов
Заглядываем под капот PostgreSQL
Чтобы оптимизировать запрос, сначала нужно понять, как база данных его выполняет. PostgreSQL использует сложный планировщик для выбора наиболее эффективного пути получения данных. К счастью, у нас есть способ заглянуть в его «мысли» — команда EXPLAIN.
EXPLAIN не выполняет запрос. Вместо этого она показывает план выполнения, который построил бы планировщик. Это как посмотреть на карту маршрута, прежде чем отправиться в путь. Вы видите шаги, но не тратите время на саму поездку.
EXPLAIN SELECT * FROM users WHERE registration_date > '2023-01-01';
Этот план показывает оценки планировщика. Но оценки могут быть неточными, например, если статистика по таблице устарела. Чтобы увидеть, что происходит на самом деле, мы добавляем ключевое слово ANALYZE.
Самый важный инструмент для оптимизации запросов — это EXPLAIN ANALYZE, который показывает, как PostgreSQL выполняет запрос:
EXPLAIN ANALYZE выполняет запрос и показывает план вместе с реальными данными о времени и количестве строк. Это уже не просто карта, а отчет GPS-трекера после поездки, с точным временем на каждом отрезке пути.
EXPLAIN ANALYZE SELECT * FROM users WHERE last_login_ip = '192.168.1.100';
Вывод этой команды может показаться пугающим, но его легко понять, если разбить на части.
Анатомия плана выполнения
План выполнения — это дерево операций, или узлов (nodes). Каждый узел представляет собой один шаг, например, сканирование таблицы или соединение. План читается снизу вверх и изнутри наружу. Самый нижний и внутренний узел выполняется первым.
В каждом узле есть ключевые метрики:
| Метрика | Описание |
|---|---|
cost | Оценка планировщика. Первое число — стоимость начала (startup cost), второе — общая стоимость (total cost). Единицы условные. |
rows | Оценочное количество строк, которое вернет узел. |
actual time | Реальное время выполнения узла в миллисекундах (только в ANALYZE). Первое число — время до получения первой строки, второе — общее время. |
rows (actual) | Фактическое количество строк, возвращенных узлом (только в ANALYZE). |
Сравнивая оценочные (cost, rows) и фактические (actual time, rows (actual)) значения, вы можете понять, где планировщик ошибся. Большое расхождение — это сигнал, что статистика по таблице устарела, и нужно запустить команду ANALYZE table_name;.
В конце вывода вы также увидите Planning Time (время на построение плана) и Execution Time (общее время выполнения запроса). Если время планирования велико, это может указывать на слишком сложный запрос.
Способы чтения данных
Основная работа базы данных — это чтение данных с диска. Способ чтения напрямую влияет на производительность. Вот основные типы сканирования, которые вы встретите в планах:
Sequential Scan
other
Полное сканирование таблицы. PostgreSQL читает каждую строку от начала до конца. Это эффективно для маленьких таблиц или когда запрос должен вернуть большую часть строк.
Если вы видите Seq Scan на большой таблице, где выбирается лишь малая часть данных, это первый признак отсутствия нужного индекса.
Index Scan
other
Сканирование с использованием индекса. Сначала PostgreSQL находит нужные записи в индексе, а затем обращается к таблице, чтобы получить сами строки. Это гораздо быстрее для выборки небольшого количества строк из большой таблицы.
Иногда вы можете увидеть Index Only Scan. Это еще более быстрый вариант, когда все необходимые для запроса данные находятся прямо в индексе, и базе данных даже не нужно обращаться к основной таблице.
Bitmap Index Scan — это гибридный подход. Сначала PostgreSQL использует индекс для создания в памяти битовой карты страниц, содержащих нужные данные. Затем он последовательно читает только эти страницы из таблицы. Это эффективно, когда нужно выбрать умеренное количество данных, которые разбросаны по таблице.
Соединение таблиц
Когда запрос включает JOIN, планировщик должен выбрать, как соединять данные из нескольких таблиц. От этого выбора сильно зависит производительность.
-
Nested Loop Join (Вложенные циклы): Самый простой метод. Для каждой строки из первой (внешней) таблицы PostgreSQL ищет совпадения во второй (внутренней) таблице. Эффективен, если внешняя таблица мала, а на поле соединения во внутренней таблице есть индекс.
-
Hash Join (Хеш-соединение): Планировщик создает в памяти хеш-таблицу по меньшей из двух таблиц. Затем он сканирует вторую таблицу и для каждой строки проверяет наличие совпадения в хеш-таблице. Отлично работает для соединения больших таблиц, когда нет подходящих индексов.
-
Merge Join (Соединение слиянием): Обе таблицы должны быть отсортированы по ключу соединения. Затем PostgreSQL одновременно читает обе таблицы и «сливает» их. Это очень быстро, если данные уже отсортированы (например, взяты из индекса).
Понимание этих концепций — первый и самый важный шаг в оптимизации. Анализируя план, вы перестаете гадать и начинаете принимать решения, основанные на данных.
Какая команда PostgreSQL выполняет запрос и показывает план выполнения вместе с фактическим временем и количеством обработанных строк?
В каком порядке следует читать узлы (операции) в плане выполнения запроса?
Теперь, когда вы знаете, как PostgreSQL думает, вы готовы находить и устранять узкие места в производительности.