zwierzeta.sql
PARTITION BY
& window functionsGrupowanie za pomocą GROUP BY
pozwala nam zagregować rekordy w grupach i wypisać wyniki tych agregatów dla każdej grupy. Tracimy w ten sposób informacje o poszczególnych rekordach. Co jednak gdybyśmy chcieli policzyć coś w obrębie zgrupowań, ale wypisać wyniki dla wszystkich rekordów? Przykładowo, chcemy dostać wszystkich pracowników i średnią zarobków z departamentu, w którym pracują.
Możemy to zrobić klasycznie, korzystając z podzapytania:
WITH dept_avg AS (
SELECT e.deptno, AVG(e.sal) AS sal
FROM emp e
GROUP BY e.deptno
)
SELECT e.*, d.sal AS dept_avg
FROM emp e
JOIN dept_avg d
ON e.deptno = d.deptno
Możemy jednak wykonać to zapytanie efektywniej, korzystając z konstrukcji PARTITION BY
:
SELECT
e.*,
AVG(e.sal) OVER (PARTITION BY e.deptno)
FROM emp e
Składnia jest następująca:
function(args) OVER (window_definition)
Gdzie w window_definition
możemy wyspecyfikować, kolejno:
aggregate_function
za pomocą PARTITION BY expression
.ORDER BY expression [ASC|DESC]
.ROWS BETWEEN from AND to
.Dobrym przykładem na ROWS
jest następujący problem: załóżmy, że chcemy policzyć sumę prefiksową pewnych wartości, w naszym przypadku zarobków pracowników. To jest chcemy posortować pracowników po zarobkach i dla -tego z nich wypisać sumę zarobków jego i wszystkich . Da się to zrobić dość skomplikowanym JOIN
-em :
SELECT
e.empno,
e.ename,
e.sal,
SUM(e2.sal) AS cumulative_sal
FROM emp e
JOIN emp e2
ON e.sal > e2.sal OR (e.sal = e2.sal AND e.empno >= e2.empno)
GROUP BY e.empno, e.ename, e.sal
ORDER BY cumulative_sal
Przy użyciu window functions da się to zrobić prościej:
SELECT
e.empno,
e.ename,
e.sal,
SUM(e.sal) OVER (
ORDER BY e.sal, e.empno
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_sum
FROM emp e
ORDER BY cumulative_sum
Napis w klauzuli OVER
mówi: posortuj wiersze po e.sal, e.empno
i następnie w tej kolejności dla każdego rozważ wszystkie poprzednie i jego samego.
Plan wykonania tego zapytania będzie o wiele prostszy - łatwo wymyślić prostą pętlę, która liczy to zapytanie, za to poprzednie będzie musiało rzeczywiście wykonać kwadratowego JOIN
a.
Żeby zobrazować działanie PARTITION BY
możemy skomplikować nasze zapytanie – nadal interesuje nas suma prefiksowa, ale tylko w obrębie departamentu.
SELECT
e.deptno,
e.empno,
e.ename,
e.sal,
SUM(e.sal) OVER (
PARTITION BY e.deptno
ORDER BY e.sal, e.empno
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_sum
FROM emp e
ORDER BY e.deptno, cumulative_sum
To zapytanie robi dokładnie to samo, ale niezależnie dla każdego departamentu wyznaczonego przez e.deptno
.
W ROWS
można podać:
UNBOUNDED PRECEDING
- wszystkie poprzednieN PRECEDING
- dokładnie N
poprzednich, gdzie N
jest stałą, np. 1 PRECEDING
UNBOUNDED FOLLOWING
- wszystkie kolejneN FOLLOWING
- dokładnie N
następnych, gdzie N
jest stałą, np. 1 FOLLOWING
CURRENT ROW
- obecny wierszJOIN
-ami tak, aby liczyło sumę oddzielnie dla każdego departamentu.PARTITION BY
aby uniknąc JOIN
-a. Dla przypomnienia: wypisz imiona oraz pensje wszystkich pracowników którzy nie mają zmiennika (osoby na tym samym stanowisku w tym samym departamencie) i posortuj ich według pensji malejąco.EXCLUDE CURRENT ROW
, Oracle SQL chyba nie, więc trzeba sprytnie wykorzystać dwa podobne okienka) (może przydać się funkcja COALESCE(args)
, która zwraca pierwszy argument nie będący NULL
em, np. COALESCE(NULL, 0)
zwraca 0
.empno, sal
1, 1000
2, 500
3, 1500
4, 1200
wynikiem powinno być
empno, next, prev
2, 1, NULL
1, 4, 2
4, 3, 1
3, NULL, 4
SELECT
e.empno,
e.deptno,
e.ename,
e.sal,
SUM(e2.sal) AS cumulative_sal
FROM emp e
JOIN emp e2
ON e.deptno = e2.deptno AND (e.sal > e2.sal OR (e.sal = e2.sal AND e.empno >= e2.empno))
GROUP BY e.empno, e.deptno, e.ename, e.sal
ORDER BY e.deptno, cumulative_sal;
WITH emp_replacements AS (
SELECT
e.ename,
e.sal,
COUNT(e.empno) OVER (PARTITION BY e.deptno, e.job) AS replacement_count
FROM emp e
)
SELECT e.ename, e.sal
FROM emp_replacements e
WHERE e.replacement_count = 1
ORDER BY e.sal DESC;
WITH emp_dept_statistics AS (
SELECT
e.empno,
e.ename,
e.deptno,
e.sal,
AVG(e.sal) OVER (PARTITION BY e.deptno) AS dept_avg,
COUNT(e.empno) OVER (PARTITION BY e.deptno) AS dept_count,
SUM(e.sal) OVER (PARTITION BY e.deptno ORDER BY e.empno ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS pred_sum,
SUM(e.sal) OVER (PARTITION BY e.deptno ORDER BY e.empno ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) AS succ_sum
FROM emp e
)
SELECT
e.empno,
e.ename,
e.deptno,
e.sal,
e.dept_avg,
e.pred_sum,
e.succ_sum,
e.dept_count,
(COALESCE(e.pred_sum, 0) + COALESCE(e.succ_sum, 0)) / (e.dept_count - 1) AS dept_avg_without_emp
FROM emp_dept_statistics e
ORDER BY dept_avg_without_emp;
SELECT
e.empno,
RANK() OVER (ORDER BY e.sal) AS "rank",
SUM(e.empno) OVER (ORDER BY e.sal, e.empno ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS "next",
SUM(e.empno) OVER (ORDER BY e.sal, e.empno ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS "prev"
FROM emp e
ORDER BY "rank", e.empno;
-- Elegancko.
SELECT
e.empno,
RANK() OVER (ORDER BY e.sal) AS "rank",
LEAD(e.empno, 1) OVER (ORDER BY e.sal, e.empno) AS "next",
LAG(e.empno, 1) OVER (ORDER BY e.sal, e.empno) AS "prev"
FROM emp e
ORDER BY "rank", e.empno;