SQLLab
Все статьи

SQL-оптимизация для Senior: вопросы на собеседовании

Вопросы и задачи по оптимизации SQL для Senior-разработчика: EXPLAIN, индексы, партиционирование, антипаттерны.

16 марта 2026 г.·4 мин чтения·

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 должен найти все):

  1. UPPER(customer_email) — функция на колонке убивает индекс по email
  2. DATE(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.

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →