Senior SQL-специалист отличается от Middle не столько объёмом знаний, сколько системным мышлением о производительности. На собеседовании это проверяется через обсуждение реальных сценариев, а не абстрактных теоретических вопросов.
Что проверять у Senior
Senior должен не просто знать про индексы — он должен принимать решения об оптимизации на основе данных. Ключевые вопросы:
- Когда создавать индекс, а когда это только навредит?
- Как читать план выполнения запроса?
- Какие антипаттерны убивают производительность?
- Когда партиционировать таблицы?
Задача 1: Чтение EXPLAIN ANALYZE
Дайте кандидату вывод EXPLAIN ANALYZE и попросите объяснить:
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.registered_at > '2025-01-01'
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 5;
Пример вывода:
HashAggregate (cost=8420.15..8895.30 rows=47515 width=36)
(actual time=234.5..267.8 rows=12453 loops=1)
Filter: (count(o.id) > 5)
-> Hash Left Join (cost=1205.50..7682.65 rows=151500 width=28)
(actual time=18.3..198.4 rows=151500 loops=1)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o (cost=0.00..2890.00 rows=151500 width=8)
(actual time=0.02..42.3 rows=151500 loops=1)
-> Hash (cost=954.00..954.00 rows=20120 width=24)
(actual time=17.8..17.8 rows=20120 loops=1)
-> Seq Scan on users u (cost=0.00..954.00 rows=20120 width=24)
(actual time=0.01..9.7 rows=20120 loops=1)
Filter: (registered_at > '2025-01-01'::date)
Rows Removed by Filter: 12580
Planning Time: 0.8 ms
Execution Time: 271.2 ms
Что ищем в ответе:
- Видит Seq Scan на users и понимает, что нужен индекс на registered_at
- Понимает, что Hash Left Join — нормальный выбор при этих объёмах
- Замечает разницу оценки (rows=47515) и факта (rows=12453) — это говорит о устаревшей статистике
- Предлагает ANALYZE для обновления статистики и CREATE INDEX
Задача 2: Диагностика медленного запроса
-- Медленный запрос, который нужно оптимизировать
SELECT *
FROM orders
WHERE UPPER(customer_email) = UPPER('user@example.com')
AND DATE(created_at) = '2026-01-15'
AND status IN ('pending', 'processing');
Проблемы (Senior должен найти все):
UPPER(customer_email)— функция на колонке убивает индекс по emailDATE(created_at) = '2026-01-15'— то же самое, DATE() блокирует индекс
Оптимизированная версия:
SELECT *
FROM orders
WHERE customer_email ILIKE 'user@example.com' -- ILIKE без функции
AND created_at >= '2026-01-15'
AND created_at < '2026-01-16' -- диапазон вместо DATE()
AND status IN ('pending', 'processing');
-- Создать function-based индекс если ILIKE нужен постоянно:
CREATE INDEX idx_orders_email_lower ON orders (LOWER(customer_email));
-- Тогда использовать: WHERE LOWER(customer_email) = LOWER('user@example.com')
Задача 3: Вопрос о типах индексов
«Когда вы бы использовали GIN индекс вместо B-tree?»
Хороший ответ: GIN оптимален для:
- Полнотекстового поиска (
tsvector) - Поиска по массивам (
@>,&&) - Поиска по JSONB полям
pg_trgmдля нечёткого поиска по строкам
B-tree оптимален для: равенства, диапазонных запросов, сортировки — большинство обычных случаев.
Задача 4: Партиционирование
-- Вопрос: «Когда партиционировать таблицу и как?»
-- Пример создания секционированной таблицы по дате в PostgreSQL 11+
CREATE TABLE orders (
id BIGINT,
customer_id INT,
amount NUMERIC,
created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2025 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
CREATE TABLE orders_2026 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
Что ищем в ответе:
- Знает, когда партиционирование помогает: при объёмах сотни миллионов строк, когда запросы фильтруют по полю партиции
- Понимает, что партиционирование не заменяет индексы
- Знает о partition pruning — как PostgreSQL пропускает нерелевантные секции
- Понимает сложность обслуживания: автоматическое создание секций, управление старыми данными
Задача 5: Антипаттерны производительности
Попросите кандидата объяснить, что не так с каждым из этих запросов:
Антипаттерн 1: SELECT с DISTINCT для устранения дубликатов
-- Часто DISTINCT — симптом проблемы в JOIN, а не решение
SELECT DISTINCT u.id, u.name
FROM users u
JOIN orders o ON o.user_id = u.id -- один JOIN уже создаёт дубликаты
Антипаттерн 2: OR вместо UNION для разных индексов
-- Плохо: OR может не использовать индексы
WHERE status = 'active' OR status = 'pending'
-- Лучше: IN (PostgreSQL оптимизирует)
WHERE status IN ('active', 'pending')
-- В крайних случаях: UNION ALL
SELECT * FROM orders WHERE status = 'active'
UNION ALL
SELECT * FROM orders WHERE status = 'pending'
Антипаттерн 3: Wildcard в начале LIKE
-- Индекс не работает при % в начале
WHERE name LIKE '%Иванов%'
-- Для полнотекстового поиска использовать tsvector или pg_trgm:
WHERE to_tsvector('russian', name) @@ to_tsquery('russian', 'Иванов')
Проверочные вопросы для Senior
- «Что такое bloat в PostgreSQL и как с ним бороться?»
- «Объясните разницу между
VACUUMиVACUUM FULL» - «Когда использовать
CREATE INDEX CONCURRENTLY?» - «Что такое
work_memи как он влияет на хэш-соединения?»
Senior, уверенно отвечающий на эти вопросы, понимает внутреннее устройство PostgreSQL, а не только синтаксис SQL.
Для глубокой подготовки к Senior-интервью рекомендуйте SQLlab.ru — там есть продвинутые задачи на оптимизацию и специфику PostgreSQL.