# Просунутий SQL для співбесід Data Analyst: підзапити, зведені таблиці та оптимізація запитів 2026 > Детальний гайд з просунутого SQL для співбесід на позицію Data Analyst: корельовані підзапити, PIVOT-запити, оптимізація через EXPLAIN та стратегії індексування. - Published: 2026-05-15 - Updated: 2026-05-15 - Author: Anthony Fillion-Maillet - Tags: sql, data-analytics, interview, query-optimization, subqueries - Reading time: 10 min --- Позиції 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 є читабельнішим і зазвичай швидшим. ## Зведені запити з умовною агрегацією в SQL Зведені (pivot) запити трансформують рядкові дані в колонкові зведення — перетворюючи місяці, збережені як рядки, в колонки на кшталт `jan_revenue`, `feb_revenue`. Хоча деякі бази даних пропонують нативний [оператор PIVOT](https://learn.microsoft.com/en-us/sql/t-sql/queries/from-using-pivot-and-unpivot), портативний підхід використовує вирази 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 з умовних виразів](https://www.postgresql.org/docs/current/functions-conditional.html) описує додаткові патерни, такі як 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](https://www.postgresql.org/docs/current/sql-explain.html) описує всі доступні формати виводу та опції. ## Стратегії індексування для оптимізації 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](/technologies/data-analytics/interview-questions/sql-subqueries-ctes) варто працювати над реальними сценаріями співбесід. Поєднання рефакторингу підзапитів з EXPLAIN-аналізом розвиває діагностичну інтуїцію, яку шукають інтерв'юери. ## Висновки - Корельовані підзапити виконуються повторно для кожного рядка — рефакторіть їх до CTE або віконних функцій для кращої продуктивності на великих наборах даних - EXISTS перевершує IN при перевірці належності, особливо коли підзапит повертає багато рядків або містить NULL - Умовна агрегація з CASE є портативною технікою зведення — вивчіть її перед синтаксисом PIVOT, специфічним для конкретної СУБД - EXPLAIN ANALYZE розкриває фактичний план виконання; зосередьтесь на Seq Scan, точності оцінки рядків та стратегії з'єднання - Порядок колонок складеного індексу повинен відповідати патернам фільтрації запитів — діє правило лівого префіксу - Часткові та покриваючі індекси зменшують I/O для цільових патернів запитів без накладних витрат повнотабличних індексів - Уникайте функцій на індексованих колонках, NOT IN з NULL та SELECT * у production-запитах - Практикуйте поєднання [віконних функцій](/blog/data-analytics/sql-window-functions-ctes-advanced-queries) з оптимізацією підзапитів для обробки багатокрокових аналітичних задач --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/data-analytics/advanced-sql-data-analyst-interviews