Hands-on benchmark exercise demonstrating the impact of SARGable vs. non-SARGable predicates.
Measure the performance difference between a function-wrapped column predicate and its SARGable range rewrite on the employes table.
EXPLAIN ANALYZE support)employes table from 00_Schema.sql (~5,000 rows in seed data)hire_date: CREATE INDEX idx_employes_hire_date ON employes (hire_date);Environment Note: Performance differences are more dramatic with larger datasets. The 5,000-row seed data demonstrates the plan-shape changes; for timing benchmarks, consider generating additional test data. Results vary by hardware, OS, buffer pool size, and cold/warm cache state.
-- Verify row count
SELECT COUNT(*) FROM employes;
-- Verify index exists (MySQL)
SHOW INDEX FROM employes;
Non-SARGable: function on indexed column.
EXPLAIN ANALYZE
SELECT emp_name, hire_date
FROM employes
WHERE EXTRACT(YEAR FROM hire_date) = 2023;
Expected plan (MySQL): Table scan on employes — the function prevents index usage.
Record: actual time, rows examined, and plan shape.
EXPLAIN ANALYZE
SELECT emp_name, hire_date
FROM employes
WHERE hire_date >= '2023-01-01'
AND hire_date < '2024-01-01';
Expected plan (MySQL): Index range scan on idx_employes_hire_date
-- MySQL InnoDB does not support INCLUDE — use a composite index instead.
-- The composite index contains all columns needed by the query, so no
-- table lookup is required (equivalent to an "Index Only Scan" in PostgreSQL).
-- CREATE INDEX idx_employes_hire_covering
-- ON employes (hire_date, emp_name);
EXPLAIN ANALYZE
SELECT emp_name, hire_date
FROM employes
WHERE hire_date >= '2023-01-01'
AND hire_date < '2024-01-01';
Expected plan (MySQL): Index range scan with Using index (covering index — no table lookup)
PostgreSQL note: PostgreSQL supports
CREATE INDEX ... INCLUDE (emp_name)for covering indexes, which adds non-key columns to leaf pages without affecting the index sort order.
MySQL 8.0.13+ supports functional/expression indexes. This version creates an index on the exact expression used in Version 0.
-- CREATE INDEX idx_employes_hire_year
-- ON employes ((YEAR(hire_date)));
EXPLAIN ANALYZE
SELECT emp_name, hire_date
FROM employes
WHERE YEAR(hire_date) = 2023;
Note: MySQL does not support partial indexes (PostgreSQL
WHEREclause onCREATE INDEX). Use expression indexes or composite indexes as alternatives.
Run each version 5 times (after one warmup run). Record the median. With only 5,000 rows, timing differences may be small; focus on plan-shape changes (access type, rows examined).
| Version | Plan Node (MySQL) | Median (ms) | Rows Examined | Access Type |
|---|---|---|---|---|
| v0: EXTRACT(YEAR) | Table scan | ALL | ||
| v1: Range rewrite | Index range scan | range | ||
| v2: Covering index | Index range scan (Using index) | range | ||
| v3: Expression index | Index ref/range | ref or range |
| Metric | v0 (baseline) | Best version | Change |
|---|---|---|---|
| Access type | ALL (table scan) | range (index) | ✓ |
| Rows examined | |||
| Median time |
-- MySQL: flush query cache and buffer pool (requires SUPER privilege)
-- RESET QUERY CACHE; -- removed in MySQL 8.0
-- FLUSH TABLES; -- closes and reopens all tables
-- For a true cold cache test, restart the MySQL server.
-- InnoDB buffer pool state persists across FLUSH TABLES.
-- On Linux: sync && echo 3 > /proc/sys/vm/drop_caches (requires root)
Note: Neither
FLUSH TABLESnor closing client connections flushes InnoDB’s buffer pool or the OS page cache — both persist across those commands. A true cold-cache test requires restarting the MySQL server and clearing the OS page cache (see above).
| Cache State | v0 (ms) | v1 (ms) |
|---|---|---|
| Cold | ||
| Warm |