ROW_NUMBER(), RANK() și DENSE_RANK() în PostgreSQL

CTE și Window Functions PostgreSQL 8.4+ (standard în PostgreSQL 15–17)

Numerotează și clasifică rândurile în interiorul unei partiții fără a le colapsa ca GROUP BY.

Sintaxă SQL
ROW_NUMBER() OVER (PARTITION BY col_grup ORDER BY col_sortare DESC)

Ce face ROW_NUMBER(), RANK() și DENSE_RANK()

Funcțiile fereastră (Window Functions) efectuează calcule peste o mulțime de rânduri înrudite cu rândul curent (definite de clauza OVER (...)), dar, spre deosebire de GROUP BY, păstrează fiecare rând individual în rezultat.

Cele trei funcții de clasament gestionează diferit egalitățile (rândurile cu aceeași valoare pe coloana din ORDER BY): ROW_NUMBER() dă fiecărui rând un număr unic consecutiv (1, 2, 3, 4); RANK() dă același loc la egalitate și lasă goluri după (1, 2, 2, 4); iar DENSE_RANK() dă același loc la egalitate fără să sară peste numere (1, 2, 2, 3).

Clauze, operatori și argumente

Clauză / Operator Descriere
ROW_NUMBER() Atribuie un număr secvențial unic (1, 2, 3…) fiecărui rând din partiție.
RANK() Clasament olimpic cu goluri la egalități (ex: 1, 2, 2, 4).
DENSE_RANK() Clasament compact fără goluri după egalități (ex: 1, 2, 2, 3).
OVER (PARTITION BY ...) Împarte rândurile în grupuri independente (partiții); numerotarea reîncepe de la 1 pentru fiecare partiție.
OVER (ORDER BY ...) Definește ordinea în care sunt numerotate și clasificate rândurile în cadrul fiecărei partiții.

Exemple

Exemplul 1: Comparația directă între ROW_NUMBER(), RANK() și DENSE_RANK()

Clasificăm angajații din fiecare departament în funcție de salariu (observă comportamentul la salariul egal 14000.00).

SELECT departament,
       nume,
       salariu,
       ROW_NUMBER() OVER (PARTITION BY departament ORDER BY salariu DESC) AS nr_rand,
       RANK()       OVER (PARTITION BY departament ORDER BY salariu DESC) AS loc_rank,
       DENSE_RANK() OVER (PARTITION BY departament ORDER BY salariu DESC) AS loc_dense
FROM angajati
WHERE departament = 'Backend';

Rezultat interogare

 departament |     nume      | salariu  | nr_rand | loc_rank | loc_dense 
-------------+---------------+----------+---------+----------+-----------
 Backend     | Maria Stan    | 16200.00 |       1 |        1 |         1
 Backend     | Andrei Popa   | 14000.00 |       2 |        2 |         2
 Backend     | Victor Toma   | 14000.00 |       3 |        2 |         2
 Backend     | Elena Lazăr   | 11500.00 |       4 |        4 |         3
(4 rows)

Exemplul 2: Eliminarea duplicatelor păstrând cel mai recent rând cu ROW_NUMBER()

Identificăm și selectăm doar primele 2 produse cel mai bine vândute din fiecare categorie folosind un CTE și ROW_NUMBER().

WITH clasament AS (
    SELECT categorie,
           denumire,
           unitati_vandute,
           ROW_NUMBER() OVER (
               PARTITION BY categorie
               ORDER BY unitati_vandute DESC, id ASC
           ) AS pozitie
    FROM produse
)
SELECT categorie, pozitie, denumire, unitati_vandute
FROM clasament
WHERE pozitie <= 2;

Rezultat interogare

 categorie | pozitie |        denumire         | unitati_vandute 
-----------+---------+-------------------------+-----------------
 Laptopuri |       1 | Laptop Office 15        |             340
 Laptopuri |       2 | ProBook Ultra 16 M3     |             215
 Monitoare |       1 | Monitor IPS 27 inch 4K  |             510
 Monitoare |       2 | Dell UltraSharp 32 4K   |             190
(4 rows)

Greșeli frecvente

  • Încercarea de a filtra o funcție fereastră direct în WHERE (ex: WHERE ROW_NUMBER() OVER (...) = 1): funcțiile fereastră se evaluează după WHERE și GROUP BY, deci trebuie încapsulate într-un CTE (WITH) sau o subinterogare.
  • Pentru „Top 1 pe fiecare grup”, SELECT DISTINCT ON este adesea mai concis și mai rapid în PostgreSQL decât ROW_NUMBER() = 1.

Comenzi și funcții înrudite

  • SELECT și DISTINCT ON: Extrage rânduri din tabele și păstrează doar primul rând pentru fiecare grup de valori distincte.
  • LAG() și LEAD(): Accesează valoarea unei coloane de pe rândul anterior (LAG) sau următor (LEAD) fără a face self-join.
  • SUM() / AVG() OVER (PARTITION BY): Calculează totaluri cumulate (running totals), medii mobile și procente din totalul grupului folosind ferestre.