QA Interview Hub · База знань · Тести
SQL
Віконні функції
ранги, top-N, LAG, накопичувальні суми
Що таке віконні функції і чим вони відрізняються від GROUP BY?
Віконна функція рахує значення по групі пов'язаних рядків («вікну»), але, на відміну від GROUP BY, не схлопує рядки: кожен рядок лишається в результаті й отримує додатковий стовпець.
Синтаксис: функція() OVER (PARTITION BY … ORDER BY …).
PARTITION BY — на які групи ділити (як GROUP BY, але без схлопування).
ORDER BY — порядок усередині групи (для рангів і накопичувальних сум).
-- кожна транзакція + загальна сума по її рахунку поруч
SELECT id, account_id, amount,
SUM(amount) OVER (PARTITION BY account_id) AS account_total
FROM transactions;
На співбесіді: Віконна функція рахує значення по групі рядків, але, на відміну від GROUP BY, не схлопує їх: кожен рядок лишається і отримує додатковий стовпець. PARTITION BY задає групи, а ORDER BY — порядок усередині. Так я можу, наприклад, показати кожну транзакцію і поруч загальну суму по її рахунку в одному запиті.
Чим відрізняються ROW_NUMBER, RANK і DENSE_RANK?
Усі три нумерують рядки в порядку ORDER BY. Різниця — в однакових значеннях:
| Зарплата | ROW_NUMBER | RANK | DENSE_RANK |
| 9000 | 1 | 1 | 1 |
| 8000 | 2 | 2 | 2 |
| 8000 | 3 | 2 | 2 |
| 7000 | 4 | 4 | 3 |
- ROW_NUMBER — завжди унікальні номери; серед однакових порядок довільний.
- RANK — однакові отримують однаковий ранг, далі пропуск.
- DENSE_RANK — однаковий ранг без пропусків.
SELECT name, salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn,
RANK() OVER (ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY salary DESC) AS drnk
FROM employees;
На співбесіді: Усі три нумерують рядки за сортуванням, різниця в однакових значеннях. ROW_NUMBER дає унікальні номери завжди, RANK дає однаковим однаковий ранг з пропуском після них, а DENSE_RANK — без пропуску. Тому для «другої найбільшої зарплати» з урахуванням повторів я беру DENSE_RANK, а для дедуплікації — ROW_NUMBER.
Як знайти топ-N записів у кожній групі?
Пронумерувати рядки всередині кожної групи віконною функцією, а потім відфільтрувати за номером. Фільтрувати віконну функцію напряму у WHERE не можна — вона обчислюється пізніше, тому потрібен CTE або підзапит.
-- 3 найбільші транзакції кожного рахунку
WITH ranked AS (
SELECT id, account_id, amount,
ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY amount DESC) AS rn
FROM transactions
WHERE status = 'completed'
)
SELECT account_id, id, amount
FROM ranked
WHERE rn <= 3
ORDER BY account_id, rn;
На співбесіді: Я нумерую рядки всередині кожної групи через ROW_NUMBER з PARTITION BY і сортуванням, а потім у зовнішньому запиті беру номери від одного до N. Віконну функцію не можна фільтрувати прямо у WHERE, бо вона обчислюється після нього, тому потрібен CTE або підзапит.
Що роблять LAG і LEAD?
LAG(col) повертає значення з попереднього рядка у вікні, LEAD(col) — з наступного. Корисно для порівняння з попереднім станом: різниця між подіями, інтервали, зміни статусу.
-- інтервал між послідовними транзакціями рахунку
SELECT account_id, id, created_at,
created_at - LAG(created_at) OVER (PARTITION BY account_id ORDER BY created_at) AS gap
FROM transactions;
-- підозріло: кілька однакових транзакцій менш ніж за 5 секунд (подвійна відправка?)
WITH g AS (
SELECT id, account_id, amount, created_at,
LAG(amount) OVER w AS prev_amount,
LAG(created_at) OVER w AS prev_at
FROM transactions
WINDOW w AS (PARTITION BY account_id ORDER BY created_at)
)
SELECT * FROM g
WHERE amount = prev_amount AND created_at - prev_at < INTERVAL '5 seconds';
На співбесіді: LAG повертає значення з попереднього рядка у вікні, LEAD — з наступного. Я використовую їх, щоб порівнювати події з попередніми: наприклад, знайти однакові транзакції з одного рахунку з інтервалом у кілька секунд — це типова ознака подвійної відправки.
Як порахувати накопичувальну суму (running total)?
SUM() OVER (… ORDER BY …) додає значення від першого рядка вікна до поточного. Так з історії транзакцій відтворюють баланс на кожен момент часу.
SELECT account_id, id, created_at, type, amount,
SUM(CASE WHEN type = 'deposit' THEN amount ELSE -amount END)
OVER (PARTITION BY account_id ORDER BY created_at, id) AS running_balance
FROM transactions
WHERE status = 'completed'
ORDER BY account_id, created_at, id;
На співбесіді: Накопичувальну суму рахує SUM з OVER і ORDER BY: до кожного рядка додається все попереднє в межах групи. Так я відтворюю баланс рахунку на кожен момент з історії транзакцій і можу знайти точку, де він став від'ємним або розійшовся з очікуваним.