JOIN și CROSS JOIN LATERAL în PostgreSQL

Interogări și filtrare specifică Postgres PostgreSQL 9.3+ (standard în PostgreSQL 15–17)

Unește tabele și permite subinterogărilor din FROM să facă referire la coloanele tabelelor precedente prin LATERAL.

Sintaxă SQL
SELECT ... FROM tabel_stanga t1 LEFT JOIN LATERAL (SELECT ... WHERE col = t1.id LIMIT n) t2 ON true;

Ce face JOIN și CROSS JOIN LATERAL

Pe lângă joncțiunile relaționale standard (INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN), PostgreSQL suportă cuvântul cheie LATERAL. O subinterogare sau o funcție de tabel precedată de LATERAL este evaluată pentru fiecare rând din tabelul aflat în stânga ei și poate accesa direct coloanele acelui rând.

CROSS JOIN LATERAL (sau LEFT JOIN LATERAL ... ON true când vrem să păstrăm rândurile fără corespondent) este soluția ideală pentru problema „Top N pe fiecare grup” sau pentru apelarea funcțiilor care returnează seturi de rânduri (precum jsonb_array_elements() sau unnest()).

Clauze, operatori și argumente

Clauză / Operator Descriere
INNER JOIN ... ON Returnează doar rândurile care au corespondent în ambele tabele conform condiției ON.
LEFT JOIN ... ON Returnează toate rândurile din tabelul stâng și completează cu NULL coloanele din dreapta dacă nu există potrivire.
CROSS JOIN LATERAL (...) Evaluează subinterogarea din dreapta pentru fiecare rând din stânga; exclude rândurile din stânga pentru care subinterogarea nu returnează nimic.
LEFT JOIN LATERAL (...) ON true La fel ca CROSS JOIN LATERAL, dar păstrează și rândurile din tabelul stâng pentru care subinterogarea returnează 0 rânduri.

Exemple

Exemplul 1: Ultimele 2 comenzi pentru fiecare client (Top-N per grup cu LATERAL)

Pentru fiecare client din tabelul clienti, subinterogarea LATERAL caută rapid cele mai recente 2 comenzi folosind indexul pe (client_id, plasata_la DESC).

SELECT c.id AS client_id,
       c.nume,
       ult.id AS comanda_id,
       ult.total_lei,
       ult.plasata_la
FROM clienti c
LEFT JOIN LATERAL (
    SELECT id, total_lei, plasata_la
    FROM comenzi cmd
    WHERE cmd.client_id = c.id
    ORDER BY cmd.plasata_la DESC
    LIMIT 2
) ult ON true
ORDER BY c.id, ult.plasata_la DESC;

Rezultat interogare

 client_id |     nume      | comanda_id | total_lei |     plasata_la      
-----------+---------------+------------+-----------+---------------------
         1 | Ana Marinescu |        912 |    350.00 | 2025-03-15 11:20:00
         1 | Ana Marinescu |        884 |    120.00 | 2025-03-02 17:05:00
         2 | Mihai Dobre   |        905 |    890.00 | 2025-03-10 09:30:00
         3 | Ioana Pavel   |            |           | 
(4 rows)

Exemplul 2: Reutilizarea expresiilor calculate în lanț cu CROSS JOIN LATERAL

Definim formule intermediare într-un bloc LATERAL (VALUES (...)) ca să nu repetăm expresii complexe în SELECT și WHERE.

SELECT p.denumire,
       p.pret_baza,
       calc.pret_redus,
       ROUND(calc.pret_redus * 1.19, 2) AS pret_cu_tva
FROM produse p
CROSS JOIN LATERAL (
    VALUES (ROUND(p.pret_baza * (1 - p.discount_proc / 100.0), 2))
) AS calc(pret_redus)
WHERE calc.pret_redus < 500;

Rezultat interogare

      denumire       | pret_baza | pret_redus | pret_cu_tva 
---------------------+-----------+------------+-------------
 SSD NVMe 1TB        |    450.00 |     405.00 |      481.95
 Mouse Wireless Pro  |    320.00 |     256.00 |      304.64
(2 rows)

Greșeli frecvente

  • Folosirea CROSS JOIN LATERAL atunci când unii părinți nu au rânduri copil: acei părinți vor dispărea complet din rezultat; folosește LEFT JOIN LATERAL (...) ON true dacă vrei să îi păstrezi.
  • Omiterea unui index pe coloana de legătură și ordonare din subinterogarea LATERAL, ceea ce ar forța câte o scanare secvențială pentru fiecare rând din tabelul stâng.

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.
  • ROW_NUMBER(), RANK() și DENSE_RANK(): Numerotează și clasifică rândurile în interiorul unei partiții fără a le colapsa ca GROUP BY.
  • ARRAY, ANY() și unnest(): Stochează și manipulează tablouri tipizate (text[], int[]), verifică apartenența cu ANY() și le extinde în rânduri cu unnest().