DSA, Database System & Operating System β Data Models, Normalization, and SQL, NEC licence examination syllabus (Nepal Engineering Council).
How each operator is physically implemented, and how results flow between them β materialization versus pipelining.
SELECT * FROM huge_table LIMIT 10 returns instantly, while SELECT * FROM huge_table ORDER BY x LIMIT 10 takes seconds. The first pipelines β it stops after ten rows. The second must sort, and a sort is a blocking operator that cannot produce its first output until it has consumed its last input. Every "why is this query slow when I only wanted 10 rows" question comes down to which operators in the plan are blocking. Search "blocking vs streaming operators query execution".-- All measured on PostgreSQL 18: student 100,000 rows /
-- 736 pages, enrol 500,000 rows / 2,703 pages.
-- ===== SEQ SCAN: reads exactly b_r pages =====
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*) FROM student WHERE dept = 'ACtE07';
-- Seq Scan on student (cost=0.00..1986.00 rows=25177)
-- (actual rows=25000.00)
-- Rows Removed by Filter: 75000
-- Buffers: shared hit=736 <- exactly b_r β
-- ===== INDEX ONLY SCAN: the heap is never touched =====
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*) FROM student WHERE marks > 90;
-- Aggregate (cost=318.04..318.05 rows=1)
-- -> Index Only Scan using idx_student_marks on student
-- (cost=0.42..292.84 rows=10081)
--
-- Cost fell from 2010 (seq scan) to 318 β a 6x improvement β
-- because COUNT(*) needs only the indexed column. The index
-- is a COVERING index for this query, so no heap fetch is
-- required at all.
-- ===== BITMAP HEAP SCAN: random I/O turned sequential ====
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*) FROM student WHERE marks = 100;
-- -> Bitmap Heap Scan on student (cost=25.18..808.89)
-- Recheck Cond: (marks = '100'::numeric)
-- Heap Blocks: exact=735
-- -> Bitmap Index Scan on idx_student_marks
-- (cost=0.00..24.89) Buffers: shared read=6
--
-- TWO-PHASE execution:
-- phase 1 read 6 index pages, build a bitmap of the heap
-- pages that contain matches
-- phase 2 sort the bitmap, read those heap pages ONCE
-- each, in physical order
--
-- Without the bitmap, a plain index scan would jump between
-- heap pages in index order and could read the same page
-- repeatedly. The bitmap converts random reads into a
-- sequential sweep.
-- "Recheck Cond" appears because a lossy bitmap may only
-- record the PAGE, so each row must be re-tested.
-- ===== SORT ELIMINATED by an index =====
EXPLAIN (ANALYZE, BUFFERS)
SELECT roll FROM student ORDER BY marks DESC LIMIT 10;
-- Limit (cost=0.42..0.97 rows=10) (actual rows=10.00)
-- Buffers: shared hit=10
-- -> Index Scan Backward using idx_student_marks
-- (cost=0.42..5556.02 rows=100000) (actual rows=10)
--
-- No Sort node at all. The index is already in marks order,
-- so scanning it BACKWARD yields descending order for free.
-- Total pages: 10.
--
-- Note the cost pair 0.42..5556.02:
-- 0.42 = startup cost (reach the first row)
-- 5556.02 = total cost (scan the whole index)
-- LIMIT 10 stops early, so the EFFECTIVE cost is 0.97.
-- That startup/total split is how the planner reasons about
-- LIMIT, and it only works because Index Scan is STREAMING.
0.42..5556.02 cost pair is worth understanding properly. The planner tracks two numbers for every node: startup cost (work before the first row emerges) and total cost (work to produce all rows). LIMIT can only exploit a low startup cost, which is why it helps enormously above a streaming operator and not at all above a Sort.
-- "top 10 students by marks" β measured three ways.
-- ===== PLAN A: no index. Sort is BLOCKING. =====
-- (index dropped for this test)
DROP INDEX IF EXISTS idx_student_marks;
EXPLAIN (ANALYZE, BUFFERS)
SELECT roll FROM student ORDER BY marks DESC LIMIT 10;
-- Limit
-- -> Sort
-- Sort Key: marks DESC
-- Sort Method: top-N heapsort Memory: 25kB
-- -> Seq Scan on student
-- Buffers: shared hit=736
--
-- All 736 pages read, then 100,000 rows sorted. The LIMIT
-- helps only in that PostgreSQL uses a top-N heapsort
-- (keeping just 10 rows) instead of a full sort β but it
-- still must SEE every row.
-- ===== PLAN B: with an index. Sort ELIMINATED. =====
CREATE INDEX idx_student_marks ON student(marks);
ANALYZE student;
EXPLAIN (ANALYZE, BUFFERS)
SELECT roll FROM student ORDER BY marks DESC LIMIT 10;
-- Limit (cost=0.42..0.97 rows=10) (actual rows=10.00)
-- Buffers: shared hit=10
-- -> Index Scan Backward using idx_student_marks
--
-- 10 pages instead of 736. No Sort node. The pipeline stops
-- after 10 rows because Index Scan is STREAMING.
-- ===== PLAN C: an aggregate. Blocking or not depends on
-- WHICH aggregate algorithm is chosen. =====
EXPLAIN (ANALYZE, BUFFERS)
SELECT dept, AVG(marks) FROM student GROUP BY dept LIMIT 1;
-- With an index on dept available:
--
-- Limit (cost=0.29..1288.54 rows=1) (actual rows=1.00)
-- Buffers: shared hit=760
-- -> GroupAggregate (cost=0.29..5153.30 rows=4)
-- Group Key: dept
-- -> Index Scan using idx_student_dept on student
-- (actual rows=25001.00)
--
-- Only 25,001 of 100,000 rows were read. GroupAggregate
-- consumes input already SORTED by the grouping key, so once
-- it has seen the last row of the first dept it can EMIT that
-- group β and LIMIT 1 then stops the scan.
--
-- Now force the hash variant:
-- SET enable_indexscan = off;
-- SET enable_bitmapscan = off;
--
-- Limit (cost=2236.00..2236.01 rows=1)
-- Buffers: shared hit=736
-- -> HashAggregate (cost=2236.00..2236.05 rows=4)
-- Group Key: dept
-- -> Seq Scan on student (actual rows=100000.00)
--
-- All 100,000 rows read. HashAggregate IS blocking: it cannot
-- know any group is complete until the input is exhausted, so
-- LIMIT 1 saves nothing.
--
-- THE REFINED RULE: LIMIT helps above a STREAMING operator.
-- Whether GROUP BY streams depends on the ALGORITHM β
-- GroupAggregate (sorted input) streams group by group;
-- HashAggregate blocks; Sort always blocks. That is why an
-- index providing grouping order can transform a
-- GROUP BY ... LIMIT query.
next() once per row, which for a million-row scan means a million virtual function calls β pure overhead. Modern engines fix this two ways: vectorized execution passes batches of ~1000 rows per call (ClickHouse, DuckDB, Snowflake), and query compilation generates machine code for the specific query so there are no operator calls at all (HyPer, and PostgreSQL's JIT for expressions). Both routinely give 10β100Γ speedups on analytical queries. Search "vectorized query execution vs compilation".b_r(2βlog_{Mβ1}(b_r/M)β + 1) and be able to work it for given b_r and M. Explain materialization versus pipelining with the intermediate-I/O cost as the difference, and state that pipelining is demand-driven via the iterator model. Finish with why LIMIT helps above a streaming operator and not above a sort or aggregate.Create a free account to tick topics off, take notes as you read, watch the video lessons and get a day-by-day study plan built around your exam date.
Loadingβ¦