SQL оптимизация для аналитика

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Зачем это знать

Начинающий аналитик: «Мой query работает 40 минут». Middle: «Я посмотрел EXPLAIN, добавил index, теперь 5 секунд». Senior: «Я перестроил модель в warehouse, теперь milliseconds».

Оптимизация SQL — навык, который делает вас в разы продуктивнее. На собесах middle+ спрашивают обязательно.

Топ приёмов

1. EXPLAIN ANALYZE

Всегда начинать с explain plan.

EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 123;
  • Seq Scan → нет indexes
  • Index Scan → используется index
  • Hash Join → parallel processing
  • Sort → ORDER BY / Window function

2. Индексы на WHERE/JOIN

CREATE INDEX idx_orders_user_id ON orders(user_id);

Composite для multi-column filters:

CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

Order важен: left-to-right match.

3. Убрать function на column

-- Плохо
WHERE DATE(created_at) = '2026-04-23'

-- Хорошо (использует index)
WHERE created_at >= '2026-04-23' AND created_at < '2026-04-24'

4. SELECT только нужные columns

-- Плохо
SELECT * FROM huge_table;

-- Хорошо
SELECT user_id, email FROM huge_table;

Меньше IO → быстрее.

5. LIMIT для exploration

SELECT * FROM big_table LIMIT 100;

Для первого взгляда на данные.

6. Filter рано

-- Плохо
SELECT * FROM big_table
JOIN small_table USING (id);

-- Лучше (filter до join)
SELECT * FROM (
    SELECT * FROM big_table WHERE condition
) b
JOIN small_table USING (id);

Less data → faster JOINs.

7. EXISTS vs IN

-- IN
SELECT * FROM a WHERE id IN (SELECT id FROM b);

-- EXISTS (often faster)
SELECT * FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.id = a.id);

Особенно на big subquery результатах.

8. Union all vs union

-- UNION удаляет дубликаты (медленнее)
SELECT * FROM a UNION SELECT * FROM b;

-- UNION ALL (быстрее, если уверены no duplicates)
SELECT * FROM a UNION ALL SELECT * FROM b;

9. Материализованные views

Для часто используемых тяжёлых queries:

CREATE MATERIALIZED VIEW daily_summary AS
SELECT DATE, SUM(total) FROM orders GROUP BY 1;

REFRESH MATERIALIZED VIEW daily_summary;  -- обновить

10. Partitioning

Для огромных tables — partition по time:

CREATE TABLE orders_2026_04 PARTITION OF orders
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');

Query по month → scan только нужный partition.

Когда что использовать

Small table (< 1M rows)

Обычно index достаточно.

Medium (1M - 100M)

Index + avoid function on column + filter early.

Large (> 100M)

Partitioning + materialized views + careful query design.

Huge (billions)

Dedicated analytics DB: ClickHouse, BigQuery, Snowflake.

Database-specific

Postgres

  • EXPLAIN (ANALYZE, BUFFERS) — detailed
  • ANALYZE table — update statistics
  • VACUUM — очистка dead tuples

ClickHouse

  • MergeTree engines
  • Partition by date
  • Primary key ≠ PK в Postgres (sort key)

BigQuery

  • Partitioned / clustered tables
  • Avoid SELECT * (billing по scanned data)
  • Materialized views

MySQL

  • SHOW INDEX
  • OPTIMIZE TABLE

Common anti-patterns

Function on indexed column

WHERE LOWER(email) = 'a' — no index use. Normalize при вставке.

Implicit type cast

WHERE user_id = '123' (number vs string) — cast breaks index.

Correlated subquery

SELECT (SELECT AVG(x) FROM t WHERE ... = o.id) FROM orders o;

Caries per row. Rewrite as JOIN.

SELECT DISTINCT *

Often symptom ошибки в JOIN multiplying rows.

CTE считается медленнее?

Postgres < 12

CTE материализуются → может замедлять.

Postgres 12+

CTE inlined → no penalty.

MSSQL, Oracle

Всегда inlined.

Window functions

Window часто быстрее self-join:

-- Slow self-join
SELECT a.*, (SELECT COUNT(*) FROM orders b WHERE b.user_id = a.user_id) 
FROM orders a;

-- Fast window
SELECT *, COUNT(*) OVER (PARTITION BY user_id) FROM orders;

На собесе

«Запрос 5 минут. Ускорь».

Алгоритм:

  1. EXPLAIN ANALYZE
  2. Identify bottleneck (Seq Scan, Sort, Hash Join)
  3. Add index, rewrite, materialize — whichever fits
  4. Test

Walk through 3-5 шагов — middle ответ. Show domain knowledge.

Связанные темы

FAQ

Best resource?

«Use the Index, Luke!» — free online book.

Index «всегда хорошо»?

Нет. Каждый index — overhead для INSERT/UPDATE.

Когда partitioning нужен?

100M rows на table, частые time-based queries.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.