SUM() OVER: totaluri cumulate și medii mobile în SQLite

Interogări avansate SQLite 3.25.0

Folosește SUM, AVG, COUNT, MIN și MAX ca funcții fereastră: totaluri cumulate, medii pe ultimele zile, procente din total.

Sintaxă
SUM(x) OVER ([PARTITION BY ...] ORDER BY ...
             [ROWS | RANGE | GROUPS BETWEEN inceput AND sfarsit]
             [EXCLUDE ...])
-- inceput/sfarsit: UNBOUNDED PRECEDING, n PRECEDING, CURRENT ROW,
--                  n FOLLOWING, UNBOUNDED FOLLOWING

Ce face SUM() OVER: totaluri cumulate și medii mobile

Orice funcție de agregare (SUM, AVG, COUNT, MIN, MAX, group_concat) devine funcție fereastră dacă îi adaugi OVER (...). Fără ORDER BY în fereastră, calculul se face peste toată partiția: SUM(suma) OVER () este totalul general, afișat pe fiecare rând.

Cu ORDER BY, cadrul implicit este RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: de la primul rând până la rândul curent, inclusiv rândurile cu aceeași valoare de ordonare. Așa obții un total cumulat. Cu ROWS BETWEEN 2 PRECEDING AND CURRENT ROW cadrul are exact ultimele trei rânduri, adică o medie mobilă.

Din SQLite 3.28.0 există și cadrele GROUPS (numără grupuri de valori egale, nu rânduri), clauza EXCLUDE (scoate rândul curent sau egalii lui din cadru) și n PRECEDING cu RANGE. Din 3.30.0, agregările acceptă și FILTER (WHERE ...).

Parametri

Parametru Descriere
ROWS Cadrul se măsoară în rânduri fizice.
RANGE Cadrul se măsoară după valoarea din ORDER BY; rândurile egale intră împreună.
GROUPS Cadrul se măsoară în grupuri de rânduri egale (3.28.0+).
EXCLUDE NO OTHERS (implicit), CURRENT ROW, GROUP sau TIES (3.28.0+).

Exemple

Exemplele de pe această pagină pornesc de la datele de mai jos. Rulează mai întâi acest cod, apoi exemplul.

CREATE TABLE incasari (
  zi       TEXT NOT NULL,
  magazin  TEXT NOT NULL,
  suma     INTEGER NOT NULL
);
INSERT INTO incasari VALUES
  ('2026-10-19', 'Iași', 500),
  ('2026-10-20', 'Iași', 700),
  ('2026-10-21', 'Iași', 300),
  ('2026-10-22', 'Iași', 900),
  ('2026-10-19', 'Cluj', 400),
  ('2026-10-20', 'Cluj', 600),
  ('2026-10-21', 'Cluj', 800);

Exemplul 1: Total cumulat pe fiecare magazin

PARTITION BY magazin face ca totalul să o ia de la zero pentru Cluj și pentru Iași. Ultima zi a fiecărui magazin arată totalul pe toată perioada.

SELECT magazin, zi, suma,
       SUM(suma) OVER (PARTITION BY magazin ORDER BY zi) AS cumulat
FROM incasari
ORDER BY magazin, zi;

Rezultat

╭─────────┬────────────┬──────┬─────────╮
│ magazin │     zi     │ suma │ cumulat │
╞═════════╪════════════╪══════╪═════════╡
│ Cluj    │ 2026-10-19 │  400 │     400 │
│ Cluj    │ 2026-10-20 │  600 │    1000 │
│ Cluj    │ 2026-10-21 │  800 │    1800 │
│ Iași    │ 2026-10-19 │  500 │     500 │
│ Iași    │ 2026-10-20 │  700 │    1200 │
│ Iași    │ 2026-10-21 │  300 │    1500 │
│ Iași    │ 2026-10-22 │  900 │    2400 │
╰─────────┴────────────┴──────┴─────────╯

Exemplul 2: Media mobilă pe ultimele 3 zile

Cadrul ROWS BETWEEN 2 PRECEDING AND CURRENT ROW are cel mult trei rânduri. În primele două zile există mai puține rânduri anterioare, așa că media se face doar pe cele disponibile. round(..., 1) păstrează o zecimală.

SELECT zi, suma,
       round(AVG(suma) OVER (ORDER BY zi ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 1) AS medie_3_zile,
       COUNT(*) OVER (ORDER BY zi ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS zile_in_cadru
FROM incasari
WHERE magazin = 'Iași'
ORDER BY zi;

Rezultat

╭────────────┬──────┬──────────────┬───────────────╮
│     zi     │ suma │ medie_3_zile │ zile_in_cadru │
╞════════════╪══════╪══════════════╪═══════════════╡
│ 2026-10-19 │  500 │        500.0 │             1 │
│ 2026-10-20 │  700 │        600.0 │             2 │
│ 2026-10-21 │  300 │        500.0 │             3 │
│ 2026-10-22 │  900 │        633.3 │             3 │
╰────────────┴──────┴──────────────┴───────────────╯

Exemplul 3: Procent din total și ziua cea mai bună

OVER () fără nimic în paranteză înseamnă toată partiția, deci totalul magazinului. Înmulțim cu 100.0 ca împărțirea să nu fie între întregi. MAX(suma) OVER (...) arată pe fiecare rând cea mai mare încasare a magazinului.

SELECT magazin, zi, suma,
       round(suma * 100.0 / SUM(suma) OVER (PARTITION BY magazin), 1) AS procent,
       MAX(suma) OVER (PARTITION BY magazin) AS maxim_magazin
FROM incasari
ORDER BY magazin, zi;

Rezultat

╭─────────┬────────────┬──────┬─────────┬───────────────╮
│ magazin │     zi     │ suma │ procent │ maxim_magazin │
╞═════════╪════════════╪══════╪═════════╪═══════════════╡
│ Cluj    │ 2026-10-19 │  400 │    22.2 │           800 │
│ Cluj    │ 2026-10-20 │  600 │    33.3 │           800 │
│ Cluj    │ 2026-10-21 │  800 │    44.4 │           800 │
│ Iași    │ 2026-10-19 │  500 │    20.8 │           900 │
│ Iași    │ 2026-10-20 │  700 │    29.2 │           900 │
│ Iași    │ 2026-10-21 │  300 │    12.5 │           900 │
│ Iași    │ 2026-10-22 │  900 │    37.5 │           900 │
╰─────────┴────────────┴──────┴─────────┴───────────────╯

Greșeli frecvente

  • Ai valori egale în coloana din ORDER BY și totalul cumulat „sare”. Cadrul implicit RANGE include toate rândurile egale cu cel curent; folosește ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW dacă vrei rând cu rând.
  • Împarți două numere întregi pentru procente: suma * 100 / total taie zecimalele. Scrie 100.0 ca rezultatul să fie real.
  • Te aștepți ca last_value(x) OVER (ORDER BY ...) să dea ultima valoare din partiție. Cu cadrul implicit dă valoarea rândului curent; scrie ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.

Comenzi înrudite

  • ROW_NUMBER, RANK, LAG (funcții fereastră): Calculează pentru fiecare rând un număr de ordine, un loc în clasament sau valoarea din rândul vecin, fără să grupeze rândurile.
  • WITH (CTE): Dă un nume unei subinterogări (Common Table Expression) și o folosește apoi ca pe un tabel, ca interogarea să fie mai ușor de citit.
  • date, time, datetime: Transformă și calculează date: extrage ziua sau ora, adună zile și luni, găsește începutul lunii sau lunea următoare.