← Назад к вопросам
Как посмотреть план и время выполнения запроса в SQL?
2.0 Middle🔥 201 комментариев
#Базы данных (SQL)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как посмотреть план и время выполнения SQL запроса
Оптимизация SQL запросов начинается с анализа их плана выполнения. Рассмотрю инструменты и методы для разных БД.
Способ 1: EXPLAIN в PostgreSQL
Основной инструмент для анализа:
-- Простой EXPLAIN
EXPLAIN
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.name
ORDER BY post_count DESC;
-- Вывод показывает план исполнения, но не реальное время
Вывод:
Sort (cost=45.32..45.33 rows=100 width=40)
Sort Key: (count(p.id)) DESC
-> HashAggregate (cost=40.23..42.23 rows=100 width=40)
Group Key: u.id, u.name
-> Hash Left Join (cost=10.00..35.20 rows=1000 width=32)
Hash Cond: (p.user_id = u.id)
-> Seq Scan on posts p (cost=0.00..20.00 rows=1000 width=8)
-> Hash (cost=5.00..5.00 rows=100 width=24)
-> Seq Scan on users u (cost=0.00..5.00 rows=100 width=24)
Способ 2: EXPLAIN ANALYZE (реальное время)
Показывает фактическое время выполнения:
-- Показывает как план так и реальное время
EXPLAIN ANALYZE VERBOSE
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.name;
Вывод:
HashAggregate (cost=40.23..42.23 rows=100 width=40)
(actual time=12.543..12.655 rows=95 width=40) <-- Реальное время
Group Key: u.id, u.name
-> Hash Left Join (cost=10.00..35.20 rows=1000 width=32)
(actual time=0.523..8.234 rows=950 width=32)
Hash Cond: (p.user_id = u.id)
Способ 3: EXPLAIN FORMAT JSON
Для программной обработки:
EXPLAIN (FORMAT JSON, ANALYZE, VERBOSE)
SELECT * FROM users WHERE id = 1;
Вывод (JSON):
[
{
"Plan": {
"Node Type": "Index Scan",
"Index Name": "users_pkey",
"Actual Rows": 1,
"Actual Time": 0.023,
"Total Cost": 0.28
},
"Execution Time": 0.045,
"Planning Time": 0.102
}
]
Способ 4: Python с psycopg2
import psycopg2
import json
conn = psycopg2.connect("dbname=mydb user=postgres")
cur = conn.cursor()
# Получить план выполнения
query = "SELECT * FROM users WHERE age > %s"
cur.execute(f"EXPLAIN (FORMAT JSON, ANALYZE) {query}", (18,))
plan = json.loads(cur.fetchone()[0])
print(json.dumps(plan, indent=2))
# Извлечь полезную информацию
for item in plan:
exec_time = item.get('Execution Time')
planning_time = item.get('Planning Time')
rows = item['Plan'].get('Actual Rows')
print(f"Время выполнения: {exec_time:.3f} ms")
print(f"Время планирования: {planning_time:.3f} ms")
print(f"Реальные строки: {rows}")
Способ 5: SQLAlchemy с EXPLAIN
from sqlalchemy import text, select
from sqlalchemy.orm import Session
def analyze_query(session: Session, query_str: str):
"""Анализирует план выполнения запроса."""
# Для PostgreSQL
explain_query = f"EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) {query_str}"
result = session.execute(text(explain_query))
plan = json.loads(result.scalar())
return plan
# Использование
from app.models import User
query = (
select(User)
.where(User.age > 18)
.order_by(User.created_at.desc())
)
query_str = str(query.compile(compile_kwargs={"literal_binds": True}))
plan = analyze_query(session, query_str)
print(f"Время: {plan[0]['Execution Time']:.2f} ms")
print(f"Строк: {plan[0]['Plan']['Actual Rows']}")
Способ 6: MySQL EXPLAIN
-- MySQL синтаксис
EXPLAIN
SELECT u.name, COUNT(p.id)
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id;
-- Вывод таблицей
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------+
| 1 | SIMPLE | u | NULL | ALL | NULL | NULL | NULL | NULL | 100 | 100.00 | NULL |
| 1 | SIMPLE | p | NULL | ref | user_id | user_id | 8 | db.u.id | 10 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------+
Ключевые колонки:
type: ALL (полный скан), INDEX, RANGE, REF — от худшего к лучшемуrows: предполагаемое число строкExtra: дополнительная информация (Using filesort, Using temporary)
Способ 7: Время выполнения в CLI
# PostgreSQL
psql -h localhost -U postgres -d mydb -c "\timing" -c "SELECT * FROM users"
# MySQL
mysql -u root -p -e "SELECT * FROM users" && echo "Time taken: "
# SQLite
sqlite3 database.db
sqlite> .timer ON
sqlite> SELECT * FROM users;
Способ 8: pg_stat_statements (PostgreSQL)
Для мониторинга реальных запросов в production:
-- Включить расширение
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Самые медленные запросы
SELECT
query,
calls,
mean_exec_time,
max_exec_time,
stddev_exec_time
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
ORDER BY mean_exec_time DESC
LIMIT 10;
Способ 9: Анализ индексов
-- Какие индексы доступны
SELECT * FROM pg_indexes WHERE tablename = 'users';
-- Какие индексы используются
EXPLAIN
SELECT * FROM users WHERE email = 'test@example.com';
-- Неиспользуемые индексы
SELECT
schemaname,
tablename,
indexname,
idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
Способ 10: BUFFERS для анализа I/O
-- Показывает сколько блоков прочитано из памяти/диска
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM large_table WHERE condition = true;
-- Вывод
/*
Seq Scan on large_table (cost=0.00..35000.00 rows=1000000 width=32)
(actual time=0.043..500.234 rows=500000 width=32)
Filter: (condition = true)
Buffers: shared hit=2000 read=1500 <-- 2000 из памяти, 1500 с диска
*/
Интерпретация плана
Красные флаги (плохие знаки)
-- ❌ Sequential Scan на большой таблице
Seq Scan on large_table (rows=1000000) <-- Полный скан!
-- ❌ Hash Join когда должен быть Nested Loop
Hash Join (rows=100000) <-- Много памяти
-- ❌ Sort (требует дополнительной памяти)
Sort (rows=50000)
-> Hash Aggregate <-- Дополнительная сортировка
-- ❌ Filter с низким процентом
Filter: (status = 'active') (rows=100000 -> 5000, filtered=5%) <-- 95% отброшено
Зелёные флаги (хорошие знаки)
-- ✅ Index Scan
Index Scan using users_id_idx (rows=1) <-- Быстро и точно
-- ✅ Nested Loop с индексом
Nested Loop (rows=10)
-> Index Scan
-> Index Scan
-- ✅ Реальные строки близко к предполагаемым
(actual 95 rows) ... (rows=100) <-- Оптимизатор угадал
Способ 11: ANALYZE TABLE (обновление статистики)
-- PostgreSQL
ANALYZE users; -- Обновить статистику
-- MySQL
ANALYZE TABLE users;
-- После этого оптимизатор будет лучше выбирать план
Практический пример (полный анализ)
import psycopg2
import json
import time
def analyze_slow_query(conn, query: str, params: tuple):
"""Полный анализ медленного запроса."""
cur = conn.cursor()
# 1. Получить план
explain_query = f"EXPLAIN (FORMAT JSON, ANALYZE, BUFFERS) {query}"
cur.execute(explain_query, params)
plan = json.loads(cur.fetchone()[0])[0]
# 2. Извлечь метрики
exec_time = plan['Execution Time']
planning_time = plan['Planning Time']
rows = plan['Plan']['Actual Rows']
buffers = plan['Plan'].get('Buffers', {})
# 3. Вывести отчёт
print("=" * 50)
print(f"Запрос: {query[:60]}...")
print("=" * 50)
print(f"Время выполнения: {exec_time:.2f} ms")
print(f"Время планирования: {planning_time:.3f} ms")
print(f"Строк получено: {rows}")
if buffers:
print(f"Буферы - Попадания в памяти: {buffers.get('shared_hit_blocks', 0)}")
print(f"Буферы - Чтение с диска: {buffers.get('shared_read_blocks', 0)}")
# 4. Рекомендации
if exec_time > 1000:
print("⚠️ МЕДЛЕННО: более 1 секунды")
elif exec_time > 100:
print("⚠️ НЕОПТИМАЛЬНО: более 100ms")
else:
print("✅ ХОРОШО: менее 100ms")
cur.close()
return plan
# Использование
conn = psycopg2.connect("dbname=mydb user=postgres")
query = "SELECT * FROM users WHERE age > %s ORDER BY created_at DESC"
analyze_slow_query(conn, query, (18,))
Лучшие практики
- EXPLAIN ANALYZE всегда для реальных запросов
- Смотрите Actual rows vs rows — если сильно отличаются, нужна ANALYZE таблицы
- Проверяйте индексы — Index Scan лучше Seq Scan
- Мониторьте в production через pg_stat_statements
- Используйте BUFFERS для I/O анализа
- Не гонитесь за миллисекундами — сначала избавьтесь от секунд
Понимание плана выполнения — ключ к оптимизации SQL запросов.