zwierzeta.sqlPARTITION 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 JOINa.
Ż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 PRECEDINGUNBOUNDED FOLLOWING - wszystkie kolejneN FOLLOWING - dokładnie N następnych, gdzie N jest stałą, np. 1 FOLLOWINGCURRENT 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 NULLem, 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;