Funkcje okienkowe

Funkcje okienkowe służa do wykonania obliczeń na zbiorze wierszy powiązanych w jakiś sposób z bieżącym wierszem. Przypomina to funkcje agregujące, lecz nie wymaga grupowania frazą GROUP BY.

Najczęściej umieszcza się je we frazie SELECT, gdzie służą do obliczeń z użyciem okna: grupy wierszy powiązanych z bieżącym wierszem zapytania. Wartości funkcji oblicza się na podstawie wszystkich wierszy wchodzących w skład bieżącego okna.

Składnia:

funkcja(argument,...) [OVER ([klauzula-dzielenia] [klauzula-sortowania]
                             [klauzula-ramki])]

gdzie klauzula-dzielenia to

   PARTITION BY wyrażenie[{,wyrażenie}...]

klauzula-sortowania to

    ORDER BY wyrażenie [ASC|DESC] [{,wyrażenie}...]

zaś klauzula-ramki

    {ROWS|RANGE} {początek-ramki|zakres-ramki}

Klauzula dzielenia PARTITION BY (ang. partition definition) grupuje wiersze wyniku w partycje, podobnie jak GROUP BY, np.

    PARTITION BY gatunek 

Klauzula sortowania ORDER BY (ang order definition) porządkuje wiersze w ramach partycji, np.

    ORDER BY waga DESC

Klauzula ramki (ang frame definition) pozwala określić wiersze, które będą użyte podczas obliczania wartości funkcji. Ramka przesuwa się ze zmianą bieżącego wiersza. Oprócz własnego zakresu, ramka jest róœnież ograniczona w ramach partycji.

Funkcje okienkowe

Wszystkie funkcje agregujące (COUNT, SUM, AVG, MAX, MIN) mogą być użyte jako funkcje okienkowe.

SELECT imie, gatunek, waga, waga / avg(waga) OVER (PARTITION BY gatunek)
FROM Zwierzaki;

Funkcja ROW_NUMBER() podaje numer kolejny wiersza w ramach partycji, zaczynając od 1. Jeśli nie było ORDER BY, to kolejność jest losowa.

SELECT gatunek, imie, wiek, 
       row_number() OVER (PARTITION BY gatunek ORDER BY wiek)
FROM Zwierzaki;

Funkcja RANK() podaje podobnie kolejne pozycje, ale

Poniższe zapytanie porządkuje zwierzaki w gatunkach według wagi.

SELECT gatunek, imie, waga, 
       rank() OVER (PARTITION BY gatunek ORDER BY waga) 
FROM Zwierzaki;

W kolejnym zapytania wszystkie zwierzaki z tego samego gatunku dostaną tę samą ,,rangę''.

SELECT imie, gatunek, waga, 
       rank() OVER (ORDER BY gatunek)
FROM Zwierzaki;

Funkcja dense_rank() działa podobnie, lecz nie zostawia pustych miejsc w numeracji.


Większe przykłady

Zestawienia zarobków pracowników ze średnimi zarobkami w ich dziale:
SELECT dname, empno, sal, avg(sal) OVER (PARTITION BY dept.deptno)
FROM emp JOIN dept on emp.deptno = dept.deptno;

  deptname | empno |  sal  |   avg          
-----------+-------+-------+-----------
 develop   |    11 |  5200 | 5020.0000
 develop   |     7 |  4200 | 5020.0000
 develop   |     9 |  4500 | 5020.0000
 develop   |     8 |  6000 | 5020.0000
 develop   |    10 |  5200 | 5020.0000
 personnel |     5 |  3500 | 3700.0000
 personnel |     2 |  3900 | 3700.0000
 sales     |     3 |  4800 | 4866.6667
 sales     |     1 |  5000 | 4866.6667
 sales     |     4 |  4800 | 4866.6667
(10 rows)

Jeśli chcielibyśmy mieć 2 najlepiej zarabiających z każdego działu, musimy użyć podzapytania:

SELECT dname, empno, sal
FROM
  (SELECT dname, empno, sal,
          rank() OVER (PARTITION BY dname ORDER BY sal DESC, empno) AS pos
     FROM emp JOIN dept on emp.deptno = dept.deptno
  ) AS ss
WHERE pos < 3;

Jeśli chcemy wywołać kilka funkcji okienkowych o tym samej strukturze ramki, można użyć frazy (klauzuli) WINDOW, żeby uniknąć kopiowania

SELECT sum(sal) OVER w, avg(sal) OVER w
  FROM emp JOIN dept on emp.deptno = dept.deptno
  WINDOW w AS (PARTITION BY dname ORDER BY sal DESC);

Tabela z informacjami o wielkości opadów w różnych dniach dla różnych miejsc, chcemy policzyć lokalną sumę opadów do danego dnia.

CREATE TABLE Opady (
  data DATE,
  miejsce VARCHAR(30),
  opad INTEGER,
  PRIMARY KEY(data,miejsce)
);

Bez okienek:

SELECT data, miejsce, opad,
       (SELECT SUM(opad)
        FROM Opady AS c2
        WHERE c2.miejsce = c1.miejsce AND
              c2.data <= c1.data) AS razem
FROM Opady AS c1
ORDER BY miejsce, data;

Dzięki okienkom unika się duplikowania:

Partition: PARTITION BY miejsce
Order: ORDER BY data
Frame: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

W sumie:

SELECT data, miejsce, opad,
       SUM(opad) OVER (PARTITION BY miejsce
                       ORDER BY data
                       ROWS BETWEEN UNBOUNDED PRECEDING
                         AND CURRENT ROW) AS razem
FROM Opady
ORDER BY miejsce, data;

Zadanie 1

1. Wypisz imiona i wiek wszystkich zwierzaków, które nie mają towarzystwa (zwierzaka tego samego gatunku) i posortuj ich według wieku malejaco. Użyj PARTITION BY i perspektywy lokalnej zamiast JOIN.

2. Dla każdego zwierzaka policz średnią wagę w jego gatunku, a także jaka byłaby ta średnia, gdyby się go pozbyć (może się przyda funkcja COALESCE(args), która zwraca pierwszy argument nie będący NULLem, np. COALESCE(NULL, 0) zwraca 0).

3. Dla każdego zwierzaka wypisz poprzednika i następnika na liście posortowanej według wagi. Dla najlżejszego zwierzaka poprzednik powinien być NULLem, a dla najcięższego następnik powinien być NULLem.

Odpowiedzi

1.1
WITH towarzystwo AS (
  SELECT imie, wiek,
         COUNT(id) OVER (PARTITION BY gatunek) AS ilu
  FROM Zwierzaki
  WHERE gatunek IS NOT NULL
)
SELECT imie, wiek
FROM towarzystwo
WHERE ilu = 1
ORDER BY wiek DESC;

1.2

WITH statystyki AS (
  SELECT imie, gatunek,
      AVG(waga) OVER (PARTITION BY gatunek) AS gat_avg,
      COUNT(id) OVER (PARTITION BY gatunek) AS gat_count,
      SUM(waga) OVER (PARTITION BY gatunek ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS pred_sum,
      SUM(waga) OVER (PARTITION BY gatunek ORDER BY id ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) AS succ_sum
    FROM Zwierzaki
)
SELECT 
    imie, gatunek,
    gat_avg,
    (COALESCE(pred_sum, 0) + COALESCE(succ_sum, 0)) / (gat_count - 1) AS gat_avg_without_this
FROM statystyki
WHERE gat_count > 1
ORDER BY 3;

1.3

SELECT 
    imie,
    RANK() OVER (ORDER BY waga) AS "rank",
    MAX(imie) OVER (ORDER BY waga, imie ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS "next",
    MAX(imie) OVER (ORDER BY waga, imie ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS "prev"
  FROM Zwierzaki
ORDER BY 2, imie;

-- Elegancko.

SELECT 
    imie,
    RANK() OVER (ORDER BY waga) AS "rank",
    LEAD(imie, 1) OVER (ORDER BY waga, imie) AS "next",
    LAG(imie, 1) OVER (ORDER BY waga, imie) AS "prev"
  FROM Zwierzaki
ORDER BY 2, imie;