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

ЁЯЧВя╕П рдзрдбрд╛ 06 тАФ Indexes: рдХрд╛рд░реНрдб рдХреЕрдЯрд▓реЙрдЧ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 06 ┬╖ рдорд╛рдЧреЗ: lesson-05-data-modelling ┬╖ рдкреБрдвреЗ: lesson-07-concurrency


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

рдзрдбреЗ 01тАУ05, рдЖрдгрд┐ рдХрд╛рд╣реА рдкреНрд░рд╢реНрдирд╛рдВрдирд╛ milliseconds рдХрд╛ рд▓рд╛рдЧрддрд╛рдд рдЖрдгрд┐ рдХрд╛рд╣реАрдВрдирд╛ рдорд┐рдирд┐рдЯреЗ рдХрд╛: indexes (рдХрд╛рд░реНрдб рдХреЕрдЯрд▓реЙрдЧ), B-trees, EXPLAIN QUERY PLAN, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдХреЕрдЯрд▓реЙрдЧ рдкреНрд░рддреНрдпреЗрдХ write рд╡рд░ рдЖрдХрд╛рд░рддреЛ рддреА рдХрд┐рдВрдордд.

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

рд╣рдЬреЗрд░реАрдЪреНрдпрд╛ рдиреЛрдВрджрд╡рд╣реАрдд 200,000 рдУрд│реА рдЖрд╣реЗрдд. "student 4242 рдХрд┐рддреА рджрд┐рд╡рд╕ рд╣рдЬрд░ рд╣реЛрддреА?" тАФ рджрдкреНрддрд░рджрд╛рд░ рдкреНрд░рддреНрдпреЗрдХ рдУрд│ рд╡рд╛рдЪрддреЛ. рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА. рд╣рд╛ SCAN: 5 рдУрд│реАрдВрд╡рд░ рдареАрдХ, 200,000 рд╡рд░ рдЪрд╣рд╛рдЪрд╛ рдмреНрд░реЗрдХ.

рдореНрд╣рдгреВрди рджрдкреНрддрд░рджрд╛рд░ рдПрдХ рдХрд╛рд░реНрдб рдХреЕрдЯрд▓реЙрдЧ ЁЯЧВя╕П рдмрдирд╡рддреЛ: student number рдиреБрд╕рд╛рд░ рд▓рд╛рд╡рд▓реЗрд▓реНрдпрд╛ cards рдЪрд╛ рдбреНрд░реЙрд╡рд░, рдкреНрд░рддреНрдпреЗрдХ card рд╕рд╛рдВрдЧрддреЗ рдХреА рддреА рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рдХреЛрдгрддреНрдпрд╛ рдУрд│реАрдВрд╡рд░ рдЖрд╣реЗ. рдЖрддрд╛ "student 4242" рдореНрд╣рдгрдЬреЗ рдереЗрдЯ рдПрдХрд╛ card рдкрд░реНрдпрдВрдд рдЪрд╛рд▓рдгреЗ тАФ рдПрдХ SEARCH. рддреЛрдЪ рдкреНрд░рд╢реНрди, 2.4 ms рд╡рд░реВрди рдореЛрдЬрддрд╛рд╣реА рди рдпреЗрдгрд╛рд▒реНрдпрд╛ рд╡реЗрд│реЗрд╡рд░.

рдХреЕрдЯрд▓реЙрдЧ рдлреБрдХрдЯ рдирд╛рд╣реА: рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдУрд│ рдЬреЛрдбрд▓реА рдХреА рддрд┐рдЪреЗ card рд╕реБрджреНрдзрд╛ рд▓рд╛рд╡рд╛рд╡реЗ рд▓рд╛рдЧрддреЗ. рджрд╣рд╛ рдХреЕрдЯрд▓реЙрдЧ рдЕрд╕рд▓реЗрд▓реА рдиреЛрдВрджрд╡рд╣реА рдкреНрд░рддреНрдпреЗрдХ рдУрд│реАрд╕рд╛рдареА рджрд╣рд╛ cards рд▓рд┐рд╣рд┐рддреЗ. рдЖрдгрд┐ 5 рдУрд│реАрдВрдЪреНрдпрд╛ рдиреЛрдВрджрд╡рд╣реАрд╕рд╛рдареАрдЪрд╛ рдХреЕрдЯрд▓реЙрдЧ рддреНрдпрд╛ 5 рдУрд│реА рд╡рд╛рдЪрдгреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рд╣рд│реВ рдЕрд╕рддреЛ. рдореНрд╣рдгреВрди: рдЬреНрдпрд╛ columns рд╡рд░ рддреБрдореНрд╣реА filter, join рдЖрдгрд┐ sort рдХрд░рддрд╛ рддреНрдпрд╛рдВрдирд╛рдЪ index рдХрд░рд╛, рдЖрдгрд┐ рдХрд╛рд╣реАрд╣реА рдмрдирд╡рдгреНрдпрд╛рдЖрдзреА рджрдкреНрддрд░рджрд╛рд░рд╛рд▓рд╛ рдорд╛рд░реНрдЧ рд╡рд┐рдЪрд╛рд░рд╛ тАФ EXPLAIN QUERY PLAN.

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

flowchart LR
    q["тЭУ SELECT COUNT(*) FROM attendance<br/>WHERE student_id = 4242"]
    scan["ЁЯЪ╢ SCAN attendance<br/>read 200,000 lines тЖТ 2.4 ms"]
    idx["ЁЯЧВя╕П CREATE INDEX idx_attendance_student<br/>ON attendance(student_id) тАФ a B-tree"]
    search["ЁЯФН SEARCH тАж USING INDEX<br/>straight to the card тЖТ 0.0 ms"]
    q -->|"1 before"| scan
    q -->|"2 after"| idx --> search
    cost["тЪЦя╕П every INSERT files a card ┬╖ composite order matters ┬╖ tiny tables: no catalogue"]
    idx -.->|"3"| cost

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "рд╡рд╛рдврдд рдЬрд╛рдИрд▓ рддрд╕реЗ рд╣рд│реВ рд╣реЛрдгрд╛рд░реЗ" app рдЖрдгрд┐ рди рд╣реЛрдгрд╛рд░реЗ app рдпрд╛рдВрддреАрд▓ рдлрд░рдХ рдЬрд╡рд│рдЬрд╡рд│ рдкреВрд░реНрдгрдкрдгреЗ рдХреЛрдгрддреНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рдВрдирд╛ рдХрд╛рд░реНрдб рдХреЕрдЯрд▓реЙрдЧ рдЖрд╣реЗ рдпрд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЕрд╕рддреЛ. рдЖрдгрд┐ index рдирд╕рд▓реЗрд▓реЗ writes рд╕реНрд╡рд╕реНрдд рдЕрд╕рддрд╛рдд рдкрдг index рдирд╕рд▓реЗрд▓реЗ reads рдЪрдХреНрд░рд╡рд╛рдвреАрдиреЗ рд╡рд╛рдврддрд╛рдд: рдЬреНрдпрд╛ рджрд┐рд╡рд╢реА table рджрд╣рд╛ рд▓рд╛рдЦ rows рдУрд▓рд╛рдВрдбрддреЗ, рддреНрдпрд╛ рджрд┐рд╡рд╢реА рд╣рд░рд╡рд▓реЗрд▓рд╛ index outage рдмрдирддреЛ.

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

db/demo.py рдордзреАрд▓ index() 200,000 rows рдЪрд╛ attendance table рдмрдирд╡рддреЗ, index рд╢рд┐рд╡рд╛рдп рдкреНрд░рд╢реНрдирд╛рдЪреА рд╡реЗрд│ рдореЛрдЬрддреЗ, index рдмрдирд╡рддреЗ, рдЖрдгрд┐ рдкреБрдиреНрд╣рд╛ рд╡реЗрд│ рдореЛрдЬрддреЗ тАФ рджреЛрдиреНрд╣реА рд╡реЗрд│рд╛ EXPLAIN QUERY PLAN рдЫрд╛рдкрдд. schema.sql рдЖрдзреАрдЪ рджреЛрди foreign keys рдирд╛ index рдХрд░рддреЗ.

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

python3 db/demo.py index
python3 - <<'EOF'
import sqlite3, time; c = sqlite3.connect("db/school.db")
for q in ["SELECT * FROM grades WHERE subject = 'maths'", "SELECT * FROM students WHERE class_id = 1 ORDER BY name"]:
    print(c.execute("EXPLAIN QUERY PLAN " + q).fetchall()[0][3], "тЖР", q)
c.execute("CREATE INDEX IF NOT EXISTS idx_grades_subject ON grades(subject)")
print(c.execute("EXPLAIN QUERY PLAN SELECT * FROM grades WHERE subject = 'maths'").fetchall()[0][3], "тЖР after the index")
# the cost side: time 20,000 inserts into attendance with the index present
t=time.perf_counter(); c.executemany("INSERT INTO attendance (student_id, day, present) VALUES (?,?,?)", [(i, '2026-01-01', 1) for i in range(20000)]); c.commit(); print(f"20,000 inserts with an index: {(time.perf_counter()-t)*1000:.0f} ms")
EOF

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

index рдЖрдзреА millisecond рд╡реЗрд│реЗрд╕рд╣ SCAN attendance рдЫрд╛рдкрддреЛ, рдордЧ ~0 ms рд╕рд╣ SEARCH attendance USING COVERING INDEX. рддреБрдордЪреА рджреБрд╕рд░реА query рдЖрдзреАрдЪ SEARCH тАж USING INDEX idx_students_class рджрд╛рдЦрд╡рддреЗ (schema.sql рдордзреАрд▓ foreign key index); рддреБрдореНрд╣реА index рдмрдирд╡рд▓реНрдпрд╛рдирдВрддрд░ grades.subject query SCAN рд╡рд░реВрди SEARCH рд╡рд░ рдЬрд╛рддреЗ; рд╡реЗрд│ рдореЛрдЬрд▓реЗрд▓реЗ inserts рджрд╛рдЦрд╡рддрд╛рдд рдХреА write рдЪреА рдХрд┐рдВрдордд рдЦрд░реА рдЖрд╣реЗ рдкрдг рд▓рд╣рд╛рди.

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

рддреБрдореНрд╣реА 200,000 рдУрд│реАрдВрдЪреЗ рдкреВрд░реНрдг рд╡рд╛рдЪрди рдПрдХрд╛ card lookup рдордзреНрдпреЗ рдмрджрд▓рд▓реЗ, planner рдЪрд╛ рдорд╛рд░реНрдЧ рдмрджрд▓рддрд╛рдирд╛ рдкрд╛рд╣рд┐рд▓рд╛, рдЖрдгрд┐ рдХреЕрдЯрд▓реЙрдЧ writes рд╡рд░ рдХрд┐рддреА рдХрд┐рдВрдордд рдШреЗрддреЛ рддреЗ рдореЛрдЬрд▓реЗ.

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

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: "index рдЬреЛрдбрд╛" рд╣рд╛ 30 seconds рдЪреНрдпрд╛ page рд▓рд╛ 30 milliseconds рдордзреНрдпреЗ рдмрджрд▓рдгрд╛рд░рд╛ рд╕рд░реНрд╡рд╛рдд рд╕рд╛рдорд╛рдиреНрдп рдПрдХрд╛ рдУрд│реАрдЪрд╛ fix рдЖрд╣реЗ тАФ рдЖрдгрд┐ write throughput рдЧреБрдкрдЪреВрдк рдЕрд░реНрдзрд╛ рдХрд░рдгрд╛рд░рд╛ рд╕рд░реНрд╡рд╛рдд рд╕рд╛рдорд╛рдиреНрдп рдПрдХрд╛ рдУрд│реАрдЪрд╛ рдмрджрд▓рд╣реА. рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдЖрдзреА рдЖрдгрд┐ рдирдВрддрд░ EXPLAIN.

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

рднрд╛рдЧ 2 рд╕реБрд░реВ рд╣реЛрддреЛ: рд░реЗрдХреЙрд░реНрдб рд░реВрдо рдЦрд▒реНрдпрд╛ рдЬрдЧрд╛рдд рдЪрд╛рд▓рд╡рдгреЗ. рдЖрдзреА, рджреЛрди clerks рдЖрдгрд┐ рдПрдХ рдиреЛрдВрджрд╡рд╣реА тАФ concurrency рдЖрдгрд┐ isolation.

git checkout lesson-07-concurrency

ЁЯЧВя╕П Lesson 06 тАФ Indexes: the card catalogue

ЁЯУН You are here: Lesson 06 of 18 ┬╖ Previous: lesson-05-data-modelling ┬╖ Next: lesson-07-concurrency


ЁЯУж What's in this branch

Lessons 01тАУ05, plus why some questions take milliseconds and some take minutes: indexes (the card catalogue), B-trees, EXPLAIN QUERY PLAN, and the cost every catalogue charges on every write.

ЁЯзТ Explain like I'm 5

The attendance register has 200,000 lines. "How many days was student 4242 present?" тАФ the archivist reads every line. Every time. That is a SCAN: fine at 5 lines, a coffee break at 200,000.

So the archivist builds a card catalogue ЁЯЧВя╕П: a drawer of cards sorted by student number, each card saying which lines that student is on. Now "student 4242" is a straight walk to one card тАФ a SEARCH. Same question, from 2.4 ms to nothing you can measure.

The catalogue is not free: every time a line is added, its card must be filed too. A register with ten catalogues writes ten cards per line. And a catalogue for a 5-line register is slower than reading the 5 lines. So: index the columns you filter, join and sort on, and ask the archivist for the route first тАФ EXPLAIN QUERY PLAN тАФ before building anything.

ЁЯЧ║я╕П Diagram

flowchart LR
    q["тЭУ SELECT COUNT(*) FROM attendance<br/>WHERE student_id = 4242"]
    scan["ЁЯЪ╢ SCAN attendance<br/>read 200,000 lines тЖТ 2.4 ms"]
    idx["ЁЯЧВя╕П CREATE INDEX idx_attendance_student<br/>ON attendance(student_id) тАФ a B-tree"]
    search["ЁЯФН SEARCH тАж USING INDEX<br/>straight to the card тЖТ 0.0 ms"]
    q -->|"1 before"| scan
    q -->|"2 after"| idx --> search
    cost["тЪЦя╕П every INSERT files a card ┬╖ composite order matters ┬╖ tiny tables: no catalogue"]
    idx -.->|"3"| cost

тЭУ What

ЁЯдФ Why

Because the difference between an app that "gets slow as it grows" and one that does not is almost entirely which questions have a card catalogue. And because unindexed writes are cheap but unindexed reads compound: the day the table crosses a million rows is the day a missing index becomes an outage.

ЁЯФз How (in this repo)

index() in db/demo.py builds a 200,000-row attendance table, times the question without an index, creates one, and times it again тАФ printing EXPLAIN QUERY PLAN both times. schema.sql already indexes the two foreign keys.

ЁЯзк Try it

python3 db/demo.py index
python3 - <<'EOF'
import sqlite3, time; c = sqlite3.connect("db/school.db")
for q in ["SELECT * FROM grades WHERE subject = 'maths'", "SELECT * FROM students WHERE class_id = 1 ORDER BY name"]:
    print(c.execute("EXPLAIN QUERY PLAN " + q).fetchall()[0][3], "тЖР", q)
c.execute("CREATE INDEX IF NOT EXISTS idx_grades_subject ON grades(subject)")
print(c.execute("EXPLAIN QUERY PLAN SELECT * FROM grades WHERE subject = 'maths'").fetchall()[0][3], "тЖР after the index")
# the cost side: time 20,000 inserts into attendance with the index present
t=time.perf_counter(); c.executemany("INSERT INTO attendance (student_id, day, present) VALUES (?,?,?)", [(i, '2026-01-01', 1) for i in range(20000)]); c.commit(); print(f"20,000 inserts with an index: {(time.perf_counter()-t)*1000:.0f} ms")
EOF

тЬЕ Verify тАФ what you should see

index prints SCAN attendance with a millisecond time, then SEARCH attendance USING COVERING INDEX with ~0 ms. Your second query already shows SEARCH тАж USING INDEX idx_students_class (the foreign key index from schema.sql); the grades.subject query flips from SCAN to SEARCH after you create the index; the timed inserts show the write cost is real but small.

ЁЯПБ What you just proved

You turned a full read of 200,000 lines into a card lookup, saw the planner's route change, and measured what the catalogue costs on writes.

тЪая╕П Common mistakes

ЁЯПн Why this matters in production: "add an index" is the most common one-line fix that turns a 30-second page into 30 milliseconds тАФ and the most common one-line change that quietly halves write throughput. EXPLAIN before and after, every time.

тПня╕П Next

Part 2 begins: running the record room for real. First, two clerks and one register тАФ concurrency and isolation.

git checkout lesson-07-concurrency
тЖР Previousdata modellingNext тЖТconcurrency

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