ЁЯПл The SchoolтА║ЁЯЧДя╕П DatabasesтА║ЁЯПЖ рдзрдбрд╛ 14 тАФ Advanced SQL: рдПрдХрд╣реА row рди рдЧрдорд╛рд╡рддрд╛ rankings
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯПЖ рдзрдбрд╛ 14 тАФ Advanced SQL: рдПрдХрд╣реА row рди рдЧрдорд╛рд╡рддрд╛ rankings

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 14 ┬╖ рдорд╛рдЧреАрд▓: lesson-13-security ┬╖ рдкреБрдвреАрд▓: lesson-15-inside-the-engine

рднрд╛рдЧ 3 тАФ рдЕрдзрд┐рдХ рдЦреЛрд▓рд╛рдд. рдзрдбреЗ 01тАУ12 рдиреА рд░реЗрдХреЙрд░реНрдб рд░реВрдо рдмрд╛рдВрдзрд▓реА рдЖрдгрд┐ рдЪрд╛рд▓рд╡рд▓реА. рдзрдбреЗ 13тАУ18 рд╣реЗ interviews рдЖрдгрд┐ incidents рдордзреНрдпреЗ рд╡рд┐рдЪрд╛рд░рд▓реЗ рдЬрд╛рдгрд╛рд░реЗ рд╡рд┐рд╖рдп рдЖрд╣реЗрдд тАФ рдкреНрд░рддреНрдпреЗрдХ рддреНрдпрд╛рдЪ school.db рд╡рд░ рдкреНрд░рддреНрдпрдХреНрд╖ рдЪрд╛рд▓рддреЛ.


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ14, рдЖрдгрд┐ db/demo.py рдордзрд▓реЗ window(): рд╢реНрд░реЗрдгреАрдВрдЪреЗ рдЧреБрдгрд╛рдВрдордзреНрдпреЗ рд░реВрдкрд╛рдВрддрд░ рдХрд░рдгрд╛рд░рд╛ рдПрдХ CTE, рдкреНрд░рддреНрдпреЗрдХ рд╡рд░реНрдЧрд╛рдд RANK() рдЖрдгрд┐ running total, "рд╢рд╛рд│реЗрдЪреНрдпрд╛ рд╕рд░рд╛рд╕рд░реАрдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд" рд╕рд╛рдареА рдПрдХ subquery, рдЖрдгрд┐ report_card рдирд╛рд╡рд╛рдЪрд╛ рдПрдХ view.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

GROUP BY рдореНрд╣рдгрдЬреЗ рдкреНрд░рддреНрдпреЗрдХ рд╡рд░реНрдЧрд╛рдХрдбреЗ рдПрдХрдЪ рдЖрдХрдбрд╛ рдорд╛рдЧрдгреЗ тАФ рд╡рд░реНрдЧрд╛рдЪреА рд╕рд░рд╛рд╕рд░реА тАФ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рд╡рд░реНрдЧрд╛рд╕рд╛рдареА рдПрдХ рдЪрд┐рдареНрдареА рдкрд░рдд рдорд┐рд│рдгреЗ. рдкрдг рдореБрдЦреНрдпрд╛рдзреНрдпрд╛рдкрд┐рдХреЗрд▓рд╛ рдпрд╛рджреАрдд рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рд╣рд╡реА рдЖрд╣реЗ, рдЖрдгрд┐ рдирд╛рд╡рд╛рд╕рдореЛрд░ рддрд┐рдЪреНрдпрд╛ рд╡рд░реНрдЧрд╛рддрд▓рд╛ рддрд┐рдЪрд╛ рдХреНрд░рдорд╛рдВрдХ рд▓рд┐рд╣рд┐рд▓реЗрд▓рд╛ рд╣рд╡рд╛. рд╣реАрдЪ window function: рддреА рд╕рдВрдмрдВрдзрд┐рдд rows рдЪреА рдПрдХ "рдЦрд┐рдбрдХреА" (рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдЪрд╛ рд╡рд░реНрдЧ) рдкрд╛рд╣рддреЗ рдЖрдгрд┐ рдПрдХрд╣реА row рди рджрд╛рдмрддрд╛ рдкреНрд░рддреНрдпреЗрдХ row рд╡рд░ рдПрдХ рдЬрд╛рд╕реНрддреАрдЪрд╛ рдЖрдХрдбрд╛ рд▓рд┐рд╣рд┐рддреЗ.

CTE (WITH тАж) рдореНрд╣рдгрдЬреЗ рддреБрдореНрд╣реА рдЖрдзреА рдмрдирд╡рддрд╛ рдЖрдгрд┐ рдордЧ рд╡рд╛рдкрд░рддрд╛ рдЕрд╕рд╛ рдЫреЛрдЯрд╛ table, рдЬрд╕реЗ рдкрд╛рдирд╛рдЪреНрдпрд╛ рдХрдбреЗрд▓рд╛ рдХрдЪреНрдЪреЗ рдХрд╛рдо рдХрд░рдгреЗ. Subquery рдореНрд╣рдгрдЬреЗ рдкреНрд░рд╢реНрдирд╛рдЪреНрдпрд╛ рдЖрдд рдкреНрд░рд╢реНрди. View рдореНрд╣рдгрдЬреЗ рддреБрдореНрд╣реА рдЬрддрди рдХрд░реВрди рдирд╛рд╡ рджрд┐рд▓реЗрд▓рд╛ рдкреНрд░рд╢реНрди.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    g["grades<br/>A+=10 ┬╖ A=9 ┬╖ B+=8 ┬╖ B=7 ┬╖ C=6"]
    cte["WITH points AS (тАж)<br/>Katrina 20 ┬╖ Meera 18 ┬╖ Aishwarya 17 ┬╖ Dipika 15 ┬╖ Rohan 13"]
    rank["RANK() OVER (PARTITION BY class ORDER BY pts DESC)<br/>3A: Katrina 1 ┬╖ Aishwarya 2 ┬╖ Dipika 3 тАФ 3B: Meera 1 ┬╖ Rohan 2"]
    sub["WHERE pts > (SELECT AVG(pts)) = 16.6<br/>Katrina ┬╖ Meera ┬╖ Aishwarya"]
    g --> cte --> rank
    cte --> sub

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/database/lesson-diagrams.html#l14

тЭУ рдХрд╛рдп

ЁЯзСтАНЁЯН│ рдЦреЛрд▓реАрдд рд░рд╛рд╣рдгрд╛рд░рд╛ code тАФ functions рдЖрдгрд┐ stored procedures

View рдкреНрд░рд╢реНрди рд╕рд╛рдард╡рддреЛ. Function SQL рдЪреНрдпрд╛ рдЖрдд рд╡рд╛рдкрд░рддрд╛ рдпреЗрдИрд▓ рдЕрд╢реА value рдкрд░рдд рджреЗрддреЛ (SELECT points(grade) тАж). Stored procedure рдПрдХ рдХрд╛рдо рдХрд░рддреЗ тАФ рдЕрдиреЗрдХ statements, рдирд┐рдпрдо рдЖрдгрд┐ writes тАФ рдЖрдгрд┐ рддреА рддреБрдореНрд╣реА CALL рдиреЗ рдЪрд╛рд▓рд╡рддрд╛. Trigger (рдзрдбрд╛ 18) row рдмрджрд▓рд▓реА рдХреА рдЖрдкреЛрдЖрдк рдЪрд╛рд▓рддреЛ.

db/postgres/procedures.sql рд╣реА рдЦрд░реА PostgreSQL рдЖрд╡реГрддреНрддреА рдЖрд╣реЗ (PostgreSQL рдЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ parser рдиреЗ рддрдкрд╛рд╕рд▓реЗрд▓реА; рдХреЛрдгрддреНрдпрд╛рд╣реА Postgres 11+ рдордзреНрдпреЗ рдЪрд╛рд▓рд╡рд╛):

CREATE PROCEDURE transfer_pupil(p_student int, p_class int)
LANGUAGE plpgsql AS $$
BEGIN
  IF (SELECT count(*) FROM students WHERE class_id = p_class) >= 3 THEN
    RAISE EXCEPTION 'class % is full (3 pupils)', p_class;
  END IF;
  UPDATE students SET class_id = p_class WHERE id = p_student;
  INSERT INTO audit_log (what) VALUES ('pupil ' || p_student || ' moved to class ' || p_class);
END
$$;

CALL transfer_pupil(4, 1);   -- ERROR:  class 1 is full (3 pupils)
CALL transfer_pupil(1, 2);   -- CALL  (and one audit_log row)
GRANT EXECUTE ON PROCEDURE transfer_pupil(int, int) TO school_api;   -- the API may CALL it, not touch the tables

SQLite рдордзреНрдпреЗ stored procedures рдирд╛рд╣реАрдд тАФ рддреЗ рддреБрдордЪреНрдпрд╛ app рдЪреНрдпрд╛ рдЖрдд рдЪрд╛рд▓рддреЗ, рдореНрд╣рдгреВрди app рд╣реАрдЪ procedure рдЖрд╣реЗ. python3 db/demo.py procs SQLite рдордзрд▓реЗ рд╕рд░реНрд╡рд╛рдд рдЬрд╡рд│рдЪреЗ рддреБрдХрдбреЗ рдЪрд╛рд▓рд╡рддреЗ: conn.create_function("points", тАж) рдиреЗ рдиреЛрдВрджрд╡рд▓реЗрд▓реЗ рдЖрдгрд┐ SQL рдЪреНрдпрд╛ рдЖрдд рд╡рд╛рдкрд░рд▓реЗрд▓реЗ Python function, рдЖрдгрд┐ RAISE(ABORT, 'class is full (3 pupils)') рдЕрд╕рд▓реЗрд▓рд╛ BEFORE UPDATE trigger, рдЬреЛ рдХреЛрдгрддреНрдпрд╛рд╣реА app рд╕рд╛рдареА рдореАрд░рд╛рдЪреЗ рднрд░рд▓реЗрд▓реНрдпрд╛ 3A рдордзреНрдпреЗ рдЬрд╛рдгреЗ рдирд╛рдХрд╛рд░рддреЛ.

ЁЯдФ рдХрд╛

Reports, leaderboards, "рдкреНрд░рддреНрдпреЗрдХ рд╡рд░реНрдЧрд╛рддрд▓реЗ рдкрд╣рд┐рд▓реЗ 3", рдорд╣рд┐рдиреНрдпрд╛-рдорд╣рд┐рдиреНрдпрд╛рдЪрд╛ рдмрджрд▓ тАФ рд╣реЗ app рдордзрд▓реНрдпрд╛ loop code рдЪреНрдпрд╛ рдкрд╛рдирд╛рдВрдРрд╡рдЬреА рдЦреЛрд▓реАрддрд▓реА рдПрдХ query рдЕрд╕рддреЗ. SQL interview рдордзрд▓реЗ рд╣реЗрдЪ рд╕рд░реНрд╡рд╛рдд рд╕рд╛рдорд╛рдиреНрдп рдкреНрд░рд╢реНрдирд╣реА рдЖрд╣реЗрдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

db/demo.py рдордзрд▓реЗ window() тАФ рдЧреБрдг CTE рдЪреНрдпрд╛ рдЖрдд CASE рдиреЗ рдореЛрдЬрд▓реЗ рдЬрд╛рддрд╛рдд, рдордЧ рдкреНрд░рддреНрдпреЗрдХ рд╡рд░реНрдЧрд╛рдд rank рдХреЗрд▓реЗ рдЬрд╛рддрд╛рдд, рд╕рд░рд╛рд╕рд░реАрд╢реА рддреБрд▓рдирд╛ рдХрд░реВрди рдЧрд╛рд│рд▓реЗ рдЬрд╛рддрд╛рдд, рдЖрдгрд┐ view рдореНрд╣рдгреВрди рдЬрддрди рд╣реЛрддрд╛рдд.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 db/demo.py window procs
python3 - <<'EOF'
import sqlite3
c = sqlite3.connect("db/school.db")
P = "CASE grade WHEN 'A+' THEN 10 WHEN 'A' THEN 9 WHEN 'B+' THEN 8 WHEN 'B' THEN 7 ELSE 6 END"
# every pupil's maths grade next to the one before it in the list (LAG) тАФ change maths to science
for row in c.execute(f"""SELECT s.name, g.grade, LAG(g.grade) OVER (ORDER BY {P} DESC) AS previous
                         FROM grades g JOIN students s ON s.id = g.student_id WHERE g.subject = 'maths'"""):
    print(row)
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

('3A', 'Katrina', 20, 1, 20), ('3A', 'Aishwarya', 17, 2, 37), ('3A', 'Dipika', 15, 3, 52), рдордЧ 3B рдкреБрдиреНрд╣рд╛ rank 1 рдкрд╛рд╕реВрди рдореАрд░рд╛рд╕рд╣ рд╕реБрд░реВ; рд╕рд░рд╛рд╕рд░реА 16.6 рдЖрд╣реЗ рдЖрдгрд┐ рдлрдХреНрдд рдХрддрд░рд┐рдирд╛, рдореАрд░рд╛ рдЖрдгрд┐ рдРрд╢реНрд╡рд░реНрдпрд╛ рддрд┐рдЪреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рд╡рд░ рдЖрд╣реЗрдд; view рддреЗрдЪ totals рдкрд░рдд рджреЗрддреЛ. рддреБрдордЪреНрдпрд╛ snippet рдордзреНрдпреЗ рдкрд╣рд┐рд▓рд╛ previous None рдЖрд╣реЗ. procs points() рдиреЗ рдХрддрд░рд┐рдирд╛, рдореАрд░рд╛, рдРрд╢реНрд╡рд░реНрдпрд╛ рдпрд╛рдВрдирд╛ rank рдХрд░рддреЗ, Meera тЖТ 3A class is full (3 pupils) рдореНрд╣рдгреВрди рдирд╛рдХрд╛рд░рддреЗ рдЖрдгрд┐ рдРрд╢реНрд╡рд░реНрдпрд╛рд▓рд╛ 3B рдордзреНрдпреЗ рд╣рд▓рд╡рддреЗ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рддреБрдореНрд╣реА groups рдЪреНрдпрд╛ рдЖрдд рдПрдХрд╣реА row рди рдЧрдорд╛рд╡рддрд╛ rows рдирд╛ rank рдХрд░реВ рд╢рдХрддрд╛, рдмреЗрд░реАрдЬ рдХрд░реВ рд╢рдХрддрд╛ рдЖрдгрд┐ рддреБрд▓рдирд╛ рдХрд░реВ рд╢рдХрддрд╛, рдЖрдгрд┐ рдПрдЦрд╛рджрд╛ рдкреНрд░рд╢реНрди рдЬрддрди рдХрд░реВ рд╢рдХрддрд╛ рдореНрд╣рдгрдЬреЗ рдкреНрд░рддреНрдпреЗрдХ app рддреЛ рдПрдХрд╛рдЪ рдкреНрд░рдХрд╛рд░реЗ рд╡рд┐рдЪрд╛рд░рддреЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: analytics pages, rankings рдЖрдгрд┐ billing reports CTEs рдЖрдгрд┐ windows рдиреЗ рд▓рд┐рд╣рд┐рд▓реЗ рдЬрд╛рддрд╛рдд; reviewers app-side loops рдРрд╡рдЬреА рд╣реЗрдЪ рдЕрдкреЗрдХреНрд╖рд┐рдд рдзрд░рддрд╛рдд.

тПня╕П рдкреБрдвреЗ

git checkout lesson-15-inside-the-engine тАФ commit рдЭрд╛рд▓реЗрд▓реА row рд╡реАрдЬ рдЧреЗрд▓реНрдпрд╛рд╡рд░рд╣реА рдХрд╛ рдЯрд┐рдХрддреЗ.

ЁЯПЖ Lesson 14 тАФ Advanced SQL: rankings without losing a row

ЁЯУН You are here: Lesson 14 of 18 ┬╖ Previous: lesson-13-security ┬╖ Next: lesson-15-inside-the-engine

Part 3 тАФ going deeper. Lessons 01тАУ12 built and ran the record room. Lessons 13тАУ18 are the topics interviews and incidents ask about тАФ each one runs for real on the same school.db.


ЁЯУж What's in this branch

Lessons 01тАУ14, plus window() in db/demo.py: a CTE that turns grades into points, RANK() and a running total inside each class, a subquery for "above the school average", and a view called report_card.

ЁЯзТ Explain like I'm 5

GROUP BY is like asking each class for one number тАФ the class average тАФ and getting one slip per class back. But the head teacher wants every pupil on the list, with their place in their class written next to their name. That is a window function: it looks at a "window" of related rows (the pupil's class) and writes an extra number on every row, without squashing any.

A CTE (WITH тАж) is a little table you build first and then use, like doing the rough work on the side of the page. A subquery is a question inside a question. A view is a question you save and give a name.

ЁЯЧ║я╕П Diagram

flowchart LR
    g["grades<br/>A+=10 ┬╖ A=9 ┬╖ B+=8 ┬╖ B=7 ┬╖ C=6"]
    cte["WITH points AS (тАж)<br/>Katrina 20 ┬╖ Meera 18 ┬╖ Aishwarya 17 ┬╖ Dipika 15 ┬╖ Rohan 13"]
    rank["RANK() OVER (PARTITION BY class ORDER BY pts DESC)<br/>3A: Katrina 1 ┬╖ Aishwarya 2 ┬╖ Dipika 3 тАФ 3B: Meera 1 ┬╖ Rohan 2"]
    sub["WHERE pts > (SELECT AVG(pts)) = 16.6<br/>Katrina ┬╖ Meera ┬╖ Aishwarya"]
    g --> cte --> rank
    cte --> sub

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/database/lesson-diagrams.html#l14

тЭУ What

ЁЯзСтАНЁЯН│ Code that lives in the room тАФ functions and stored procedures

A view stores a question. A function returns a value you can use inside SQL (SELECT points(grade) тАж). A stored procedure does a job тАФ several statements, rules and writes тАФ and you run it with CALL. A trigger (lesson 18) runs by itself when a row changes.

db/postgres/procedures.sql is the real PostgreSQL version (checked with PostgreSQL's own parser; run it in any Postgres 11+):

CREATE PROCEDURE transfer_pupil(p_student int, p_class int)
LANGUAGE plpgsql AS $$
BEGIN
  IF (SELECT count(*) FROM students WHERE class_id = p_class) >= 3 THEN
    RAISE EXCEPTION 'class % is full (3 pupils)', p_class;
  END IF;
  UPDATE students SET class_id = p_class WHERE id = p_student;
  INSERT INTO audit_log (what) VALUES ('pupil ' || p_student || ' moved to class ' || p_class);
END
$$;

CALL transfer_pupil(4, 1);   -- ERROR:  class 1 is full (3 pupils)
CALL transfer_pupil(1, 2);   -- CALL  (and one audit_log row)
GRANT EXECUTE ON PROCEDURE transfer_pupil(int, int) TO school_api;   -- the API may CALL it, not touch the tables

SQLite has no stored procedures тАФ it runs inside your app, so the app is the procedure. python3 db/demo.py procs runs the closest SQLite pieces: a Python function registered with conn.create_function("points", тАж) and used inside SQL, and a BEFORE UPDATE trigger with RAISE(ABORT, 'class is full (3 pupils)') that refuses Meera's move into a full 3A for any app.

ЁЯдФ Why

Reports, leaderboards, "top 3 per class", month-over-month change тАФ these are one query in the room instead of pages of loop code in the app. They are also the most common SQL interview questions.

ЁЯФз How (in this repo)

window() in db/demo.py тАФ the points are computed with a CASE inside a CTE, then ranked per class, filtered against the average, and saved as a view.

ЁЯзк Try it

python3 db/demo.py window procs
python3 - <<'EOF'
import sqlite3
c = sqlite3.connect("db/school.db")
P = "CASE grade WHEN 'A+' THEN 10 WHEN 'A' THEN 9 WHEN 'B+' THEN 8 WHEN 'B' THEN 7 ELSE 6 END"
# every pupil's maths grade next to the one before it in the list (LAG) тАФ change maths to science
for row in c.execute(f"""SELECT s.name, g.grade, LAG(g.grade) OVER (ORDER BY {P} DESC) AS previous
                         FROM grades g JOIN students s ON s.id = g.student_id WHERE g.subject = 'maths'"""):
    print(row)
EOF

тЬЕ Verify тАФ what you should see

('3A', 'Katrina', 20, 1, 20), ('3A', 'Aishwarya', 17, 2, 37), ('3A', 'Dipika', 15, 3, 52), then 3B starting again at rank 1 with Meera; the average is 16.6 and only Katrina, Meera and Aishwarya are above it; the view returns the same totals. In your snippet the first previous is None. procs ranks Katrina, Meera, Aishwarya with points(), refuses Meera тЖТ 3A with class is full (3 pupils) and moves Aishwarya to 3B.

ЁЯПБ What you just proved

You can rank, total and compare rows inside groups without losing any, and save a question so every app asks it the same way.

тЪая╕П Common mistakes

ЁЯПн Why this matters in production: analytics pages, rankings and billing reports are written with CTEs and windows; reviewers expect them instead of app-side loops.

тПня╕П Next

git checkout lesson-15-inside-the-engine тАФ why a committed row survives a power cut.

тЖР PrevioussecurityNext тЖТinside the engine

This page is the lesson's README from the lesson-14-advanced-sql branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.