Просунутий SQL для співбесід Data Analyst: підзапити, зведені таблиці та оптимізація запитів 2026

Детальний гайд з просунутого SQL для співбесід на позицію Data Analyst: корельовані підзапити, PIVOT-запити, оптимізація через EXPLAIN та стратегії індексування.

Просунутий SQL для співбесід на позицію Data Analyst: підзапити, зведені таблиці та оптимізація запитів

Позиції Data Analyst залишаються одними з найбільш затребуваних на ринку IT у 2026 році. Роботодавці в таких компаніях як Meta, Stripe та Airbnb тестують кандидатів на корельованих підзапитах, pivot-трансформаціях та оптимізації запитів — навичках, які відрізняють junior-аналітиків від senior-фахівців. Цей гайд охоплює саме ті патерни, які з'являються на технічних етапах відбору, з production-ready прикладами на PostgreSQL 17.

Що насправді перевіряють на співбесідах

SQL-співбесіди для Data Analyst у 2026 році фокусуються на трьох напрямках: декомпозиція складної логіки на читабельні підзапити або CTE, трансформація рядкових даних у pivot-звіти за допомогою умовної агрегації та демонстрація розуміння продуктивності запитів через EXPLAIN-плани та індексування. Більшість кандидатів провалюється на оптимізації — знання цієї теми дає суттєву перевагу.

Корельовані підзапити проти звичайних підзапитів на SQL-співбесідах

Корельований підзапит посилається на колонку із зовнішнього запиту, що змушує рушій бази даних перераховувати підзапит для кожного рядка в зовнішньому результуючому наборі. Звичайний (некорельований) підзапит виконується один раз і повертає фіксований результат. Інтерв'юери спеціально перевіряють, чи розуміють кандидати, коли кожен підхід доречний — і коли JOIN працює краще за обидва.

Звичайний підзапит добре працює для порогових порівнянь. Внутрішній запит виконується один раз, а зовнішній запит фільтрує за цим єдиним значенням.

sql
-- regular_subquery_threshold.sql
-- Find all orders above the average order value
SELECT
  order_id,
  customer_id,
  total_amount
FROM orders
WHERE total_amount > (
  -- Executes once, returns a single scalar value
  SELECT AVG(total_amount)
  FROM orders
);

Корельований підзапит необхідний, коли порівняння залежить від контексту кожного рядка. Класичне питання на співбесіді: "Знайдіть працівників, які заробляють більше за середню зарплату у своєму відділі."

sql
-- correlated_subquery_department.sql
-- Employees earning above their department average
SELECT
  e.employee_id,
  e.name,
  e.department,
  e.salary
FROM employees e
WHERE e.salary > (
  -- Re-evaluated for each employee's department
  SELECT AVG(e2.salary)
  FROM employees e2
  WHERE e2.department = e.department
);

Корельована версія повторно обчислює AVG для відділу кожного рядка. На великих таблицях це стає витратним. Оптимізований підхід використовує CTE або віконну функцію для одноразового обчислення середніх значень по відділах.

sql
-- optimized_with_cte.sql
-- Same result, single pass over the data
WITH dept_avg AS (
  SELECT
    department,
    AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department
)
SELECT
  e.employee_id,
  e.name,
  e.department,
  e.salary
FROM employees e
JOIN dept_avg d ON e.department = d.department
WHERE e.salary > d.avg_salary;

Інтерв'юери шукають кандидатів, які здатні написати корельовану версію, а потім відрефакторити її. Демонстрація обох підходів — з поясненням компромісу продуктивності — сигналізує про senior-рівень володіння SQL.

Патерни підзапитів на співбесідах для Data Analyst

Три патерни підзапитів регулярно з'являються на співбесідах: EXISTS для перевірки належності, скалярні підзапити в SELECT та похідні таблиці у FROM.

EXISTS проти IN — EXISTS зупиняється одразу після знаходження першого збігу, що робить його швидшим за IN для великих результуючих наборів. IN матеріалізує весь результат підзапиту перед порівнянням.

sql
-- exists_vs_in.sql
-- Customers who placed at least one order in 2026 (EXISTS — preferred)
SELECT c.customer_id, c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.customer_id
    AND o.order_date >= '2026-01-01'
);

-- Equivalent with IN (slower on large datasets)
SELECT customer_id, name
FROM customers
WHERE customer_id IN (
  SELECT customer_id
  FROM orders
  WHERE order_date >= '2026-01-01'
);

Скалярний підзапит у SELECT — корисний для додавання обчислюваної колонки без повного JOIN.

sql
-- scalar_subquery_select.sql
-- Each product with its category's total revenue
SELECT
  p.product_id,
  p.product_name,
  p.category,
  (
    SELECT SUM(oi.quantity * oi.unit_price)
    FROM order_items oi
    JOIN products p2 ON oi.product_id = p2.product_id
    WHERE p2.category = p.category
  ) AS category_total_revenue
FROM products p;

Цей патерн зручний для додавання однієї-двох колонок. Для більшої кількості CTE з JOIN є читабельнішим і зазвичай швидшим.

Готовий до співбесід з Data Analytics?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Зведені запити з умовною агрегацією в SQL

Зведені (pivot) запити трансформують рядкові дані в колонкові зведення — перетворюючи місяці, збережені як рядки, в колонки на кшталт jan_revenue, feb_revenue. Хоча деякі бази даних пропонують нативний оператор PIVOT, портативний підхід використовує вирази CASE всередині агрегатних функцій. Саме цю версію очікують інтерв'юери.

Зведений запит поєднує умовну агрегацію з виразами CASE всередині агрегатних функцій. Кожен вираз CASE фільтрує конкретну категорію, а агрегат (SUM, COUNT, AVG) згортає відфільтровані рядки до єдиного значення на групу.

sql
-- pivot_monthly_revenue.sql
-- Monthly revenue pivot: one row per product, one column per month
SELECT
  product_id,
  product_name,
  SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 1
    THEN quantity * unit_price ELSE 0 END) AS jan_revenue,
  SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 2
    THEN quantity * unit_price ELSE 0 END) AS feb_revenue,
  SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 3
    THEN quantity * unit_price ELSE 0 END) AS mar_revenue,
  SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 4
    THEN quantity * unit_price ELSE 0 END) AS apr_revenue,
  -- Repeat for remaining months
  SUM(quantity * unit_price) AS total_revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date >= '2026-01-01'
GROUP BY product_id, product_name
ORDER BY total_revenue DESC;

Та сама техніка умовної агрегації працює для багатовимірних зведень. Поширене питання на співбесіді просить кандидатів побудувати звіт активності користувачів за днем тижня та типом пристрою.

sql
-- pivot_user_activity.sql
-- User activity: sessions by day of week and device type
SELECT
  user_id,
  COUNT(CASE WHEN device_type = 'mobile' THEN 1 END) AS mobile_sessions,
  COUNT(CASE WHEN device_type = 'desktop' THEN 1 END) AS desktop_sessions,
  COUNT(CASE WHEN device_type = 'tablet' THEN 1 END) AS tablet_sessions,
  ROUND(
    COUNT(CASE WHEN device_type = 'mobile' THEN 1 END) * 100.0 / COUNT(*),
    1
  ) AS mobile_pct
FROM user_sessions
WHERE session_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY user_id
HAVING COUNT(*) >= 5  -- Only users with meaningful activity
ORDER BY mobile_pct DESC;

Умовна агрегація з CASE є фундаментальною навичкою Data Analyst. Документація PostgreSQL з умовних виразів описує додаткові патерни, такі як FILTER-клаузи, що спрощують цей синтаксис.

Динамічний PIVOT з CROSSTAB у PostgreSQL

Розширення tablefunc PostgreSQL надає CROSSTAB для випадків, коли категорії зведення невідомі на момент написання запиту. Хоча менш поширене на співбесідах, воно зустрічається в домашніх завданнях.

sql
-- crosstab_dynamic_pivot.sql
-- Enable the extension (once per database)
CREATE EXTENSION IF NOT EXISTS tablefunc;

-- Revenue by product and quarter using CROSSTAB
SELECT *
FROM crosstab(
  $$
    SELECT
      product_name,
      'Q' || EXTRACT(QUARTER FROM order_date)::TEXT AS quarter,
      SUM(quantity * unit_price) AS revenue
    FROM order_items oi
    JOIN orders o ON oi.order_id = o.order_id
    JOIN products p ON oi.product_id = p.product_id
    WHERE EXTRACT(YEAR FROM o.order_date) = 2026
    GROUP BY product_name, quarter
    ORDER BY product_name, quarter
  $$,
  $$ VALUES ('Q1'), ('Q2'), ('Q3'), ('Q4') $$
) AS pivot_table(
  product_name TEXT,
  q1_revenue NUMERIC,
  q2_revenue NUMERIC,
  q3_revenue NUMERIC,
  q4_revenue NUMERIC
);

CROSSTAB вимагає, щоб вихідний запит повертав рівно три колонки: ідентифікатор рядка, категорію та значення. Другий аргумент перелічує всі можливі значення категорій. Відсутні комбінації генерують NULL замість 0.

Оптимізація SQL-запитів: EXPLAIN-плани та аналіз вартості

Питання з оптимізації запитів перевіряють, чи може кандидат діагностувати повільні запити — не просто писати правильні. Основним діагностичним інструментом є EXPLAIN ANALYZE, який виконує запит і повідомляє фактичні часи виконання, кількість рядків та операції, обрані планувальником.

sql
-- explain_analyze_example.sql
-- Analyze a slow query to identify bottlenecks
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT
  c.customer_id,
  c.name,
  COUNT(o.order_id) AS order_count,
  SUM(o.total_amount) AS lifetime_value
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_date >= '2025-01-01'
GROUP BY c.customer_id, c.name
HAVING SUM(o.total_amount) > 1000
ORDER BY lifetime_value DESC;

Ключові елементи виводу EXPLAIN, про які запитують інтерв'юери:

  • Seq Scan — повне сканування таблиці. Прийнятне на малих таблицях; проблематичне на мільйонах рядків. Вказує на відсутній індекс.
  • Index Scan / Index Only Scan — запит використовує індекс. Index Only Scan швидший, оскільки читає дані безпосередньо з індексу без звернення до таблиці.
  • Nested Loop / Hash Join / Merge Join — стратегія з'єднання. Hash Join підходить для великих несортованих наборів. Merge Join оптимальний, коли обидві сторони попередньо відсортовані. Nested Loop працює для малих зовнішніх таблиць, з'єднаних з індексованими внутрішніми.
  • Rows — оцінена проти фактичної кількості рядків. Великі розбіжності свідчать про застарілу статистику (запустіть ANALYZE) або слабо селективні предикати.
  • Buffers: shared hit / read — коефіцієнт влучень у кеш. Високі значення read означають, що дані не в пам'яті.

Документація PostgreSQL з EXPLAIN описує всі доступні формати виводу та опції.

Стратегії індексування для оптимізації SQL на рівні співбесіди

Індексування є найвпливовішою технікою оптимізації та частою темою співбесід. Питання зазвичай має форму: "Маючи цей повільний запит, які індекси покращать його?"

Три патерни індексування покривають більшість сценаріїв на співбесідах:

1. Одноколонковий індекс на високоселективних WHERE-клаузах

sql
-- indexing_strategies.sql
-- Index on order_date: filters a small percentage of rows
CREATE INDEX idx_orders_date ON orders (order_date);

-- Composite index for queries filtering on both columns
CREATE INDEX idx_orders_customer_date
  ON orders (customer_id, order_date);

-- Covering index: includes columns needed in SELECT
-- Enables Index Only Scan (no table access)
CREATE INDEX idx_orders_covering
  ON orders (customer_id, order_date)
  INCLUDE (total_amount, status);

2. Порядок колонок у складеному індексі має значення. Найлівіша колонка повинна відповідати WHERE-клаузі запиту. Індекс на (customer_id, order_date) прискорює запити з фільтрацією по customer_id або по customer_id AND order_date, але НЕ запити з фільтрацією лише по order_date.

3. Часткові індекси зменшують розмір індексу та витрати на обслуговування, коли часто запитується лише підмножина рядків.

sql
-- partial_index.sql
-- Only index active orders (smaller, faster index)
CREATE INDEX idx_orders_active
  ON orders (customer_id, order_date)
  WHERE status = 'active';

-- Only index recent data for dashboard queries
CREATE INDEX idx_orders_recent
  ON orders (order_date)
  WHERE order_date >= '2026-01-01';
Антипатерни індексування

Слід уникати індексування кожної колонки — кожен індекс уповільнює операції INSERT, UPDATE та DELETE. Таблиця з понад 10 індексами при інтенсивних операціях запису зазнає помітної деградації. Також слід уникати індексування колонок з низькою кардинальністю (як boolean-прапорці), якщо вони не є частиною складеного або часткового індексу.

Поширені антипатерни запитів, що вбивають продуктивність

Інтерв'юери подають навмисно повільні запити та просять кандидатів ідентифікувати проблему. Ці п'ять антипатернів відповідають за більшість реальних проблем з продуктивністю.

Функції на індексованих колонках — застосування функції до індексованої колонки унеможливлює використання індексу.

sql
-- anti_patterns.sql
-- BAD: function on indexed column disables the index
SELECT * FROM orders
WHERE EXTRACT(YEAR FROM order_date) = 2026;

-- GOOD: range comparison uses the index
SELECT * FROM orders
WHERE order_date >= '2026-01-01'
  AND order_date < '2027-01-01';

SELECT * у production-запитах — витягує всі колонки, включаючи великі поля TEXT або BYTEA. Вказуйте лише потрібні колонки.

NOT IN з NULL — якщо підзапит повертає будь-яке NULL, NOT IN не повертає жодного рядка. Замініть на NOT EXISTS.

sql
-- not_in_null_trap.sql
-- BAD: returns empty if any customer_id is NULL in subquery
SELECT * FROM customers
WHERE customer_id NOT IN (
  SELECT customer_id FROM orders
);

-- GOOD: NOT EXISTS handles NULLs correctly
SELECT * FROM customers c
WHERE NOT EXISTS (
  SELECT 1 FROM orders o
  WHERE o.customer_id = c.customer_id
);

Неявне приведення типів — порівняння VARCHAR-колонки з цілим числом змушує виконувати приведення для кожного рядка, оминаючи індекс. Завжди явно узгоджуйте типи.

Відсутній LIMIT у дослідницьких запитах — сканування мільйонів рядків, коли потрібні лише перші 10. Завжди додавайте LIMIT під час розробки та в пагінованих API-запитах.

Практикуйте ці патерни

Для розвитку навичок роботи з підзапитами та CTE в SQL варто працювати над реальними сценаріями співбесід. Поєднання рефакторингу підзапитів з EXPLAIN-аналізом розвиває діагностичну інтуїцію, яку шукають інтерв'юери.

Готовий до співбесід з Data Analytics?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Висновки

  • Корельовані підзапити виконуються повторно для кожного рядка — рефакторіть їх до CTE або віконних функцій для кращої продуктивності на великих наборах даних
  • EXISTS перевершує IN при перевірці належності, особливо коли підзапит повертає багато рядків або містить NULL
  • Умовна агрегація з CASE є портативною технікою зведення — вивчіть її перед синтаксисом PIVOT, специфічним для конкретної СУБД
  • EXPLAIN ANALYZE розкриває фактичний план виконання; зосередьтесь на Seq Scan, точності оцінки рядків та стратегії з'єднання
  • Порядок колонок складеного індексу повинен відповідати патернам фільтрації запитів — діє правило лівого префіксу
  • Часткові та покриваючі індекси зменшують I/O для цільових патернів запитів без накладних витрат повнотабличних індексів
  • Уникайте функцій на індексованих колонках, NOT IN з NULL та SELECT * у production-запитах
  • Практикуйте поєднання віконних функцій з оптимізацією підзапитів для обробки багатокрокових аналітичних задач

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#sql
#data-analytics
#interview
#query-optimization
#subqueries

Поділитися

Пов'язані статті