Q1When the result of EXPLAIN QUERY PLAN changes from SCAN perf_sales to SEARCH perf_sales USING INDEX ..., what does it mean?
Indexes — Composite, Partial, Expression, and UNIQUE
On the 50,000-row perf_sales table, you'll create indexes with CREATE INDEX, build composite indexes, a partial index on status='pending', an expression index on amount/100, and a UNIQUE index on sale_id, then watch EXPLAIN QUERY PLAN switch from FULL SCAN (SCAN) to INDEX SCAN (SEARCH).
The dataset for this article — the perf_sales table
An index works like the index in the back of a book: it lets you find rows with a specific column value quickly.
This article walks through creating indexes with CREATE INDEX, building composite, partial, expression, and UNIQUE indexes, and using EXPLAIN QUERY PLAN to confirm how the lookup strategy changes.
CREATE INDEX and EXPLAIN QUERY PLAN — turn a full table scan into an index seek
Create an index with CREATE INDEX index_name ON table(column);.
When you use an indexed column in a WHERE equality or range condition, the optimizer can pick an index lookup instead of a full table scan and jump straight to the matching rows.
Check the lookup strategy with EXPLAIN QUERY PLAN.
Prefix your query with EXPLAIN QUERY PLAN and you'll see the plan for how the rows will be found instead of the actual results.
Without an index you'll see SCAN perf_sales (also called a FULL SCAN — examine every row in the table), and once an index kicks in you'll see something like SEARCH perf_sales USING INDEX ... (also called an INDEX SCAN — narrow the search with the index).
The exact wording of the plan varies by environment, so this article treats a successful run as the correct answer and reads the meaning of the plan in the prose and explanations.
-- Plan without an index (separate read-only example)
EXPLAIN QUERY PLAN
SELECT sale_id, amount FROM perf_sales WHERE region = 'East';
--> SCAN perf_sales (examine every row)
-- Create an index, then look at the same plan
DROP INDEX IF EXISTS ix_region;
CREATE INDEX ix_region ON perf_sales(region);
EXPLAIN QUERY PLAN
SELECT sale_id, amount FROM perf_sales WHERE region = 'East';
--> SEARCH perf_sales USING INDEX ix_region (region=?)
-- Note: run DROP -> CREATE -> EXPLAIN end-to-end in the same execution
Composite indexes — index multiple columns together
Writing several columns inside the parentheses — CREATE INDEX name ON table(col1, col2); — produces a composite index (also called a multi-column index).
It speeds up queries that use the conditions from the leading column onward, like WHERE col1 = ... AND col2 >= ....
Column order matters: a composite index won't help queries that don't use its leading column in the WHERE clause.
If your access pattern is fixed — say "equality on emp_id and a range on sale_date" — building a composite index in that order lets one index cover both conditions.
-- Composite index (separate read-only example)
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(region, sale_date);
EXPLAIN QUERY PLAN
SELECT sale_id FROM perf_sales
WHERE region = 'East' AND sale_date >= '2024-01-01';
--> SEARCH perf_sales USING INDEX ix_demo (region=? AND sale_date>?)
-- Narrow by region (equality), then further narrow sale_date by range
Partial indexes and expression indexes
A partial index is built with CREATE INDEX name ON table(column) WHERE condition; and indexes only a subset of the rows in the table.
If you frequently search rows with a specific value such as status = 'pending', indexing only those rows keeps the index small and focused on the queries you actually run.
In perf_sales, only 5,000 rows (10% of the total) have status = 'pending', so a partial index with WHERE status = 'pending' is a natural fit.
An expression index is built with CREATE INDEX name ON table(expression); and indexes the result of a computed expression rather than a raw column.
Use it for queries that apply the same expression on the search side, such as WHERE amount / 100 = ....
A regular index on a column normally doesn't help once you apply a function or arithmetic to that column, but if you index the expression itself, searches using the matching expression can use it.
-- Partial index (separate read-only example)
DROP INDEX IF EXISTS ix_part;
CREATE INDEX ix_part ON perf_sales(sale_date) WHERE status = 'refunded';
EXPLAIN QUERY PLAN
SELECT sale_id FROM perf_sales
WHERE status = 'refunded' AND sale_date >= '2024-01-01';
--> SEARCH perf_sales USING ... INDEX ix_part (sale_date>?)
-- Expression index
DROP INDEX IF EXISTS ix_expr;
CREATE INDEX ix_expr ON perf_sales(qty * amount);
EXPLAIN QUERY PLAN
SELECT sale_id FROM perf_sales WHERE qty * amount = 100000;
--> SEARCH perf_sales USING ... INDEX ix_expr (<expr>=?)
UNIQUE indexes — an index that disallows duplicates
A UNIQUE index — built with CREATE UNIQUE INDEX name ON table(column); — speeds up searches and prevents duplicate values from being inserted into the column.
If you try to create a UNIQUE index on a column that already contains duplicates, you'll get a UNIQUE constraint failed error and the index won't be created at all.
The flip side: if you create one on a column whose values are unique (a primary key or a unique business key), you get duplicate prevention plus a fast equality lookup.
In perf_sales, product (200 distinct values) has many repeats, so a UNIQUE index isn't possible there. sale_id, however, is the primary key and unique, so you can build one.
The next exercise creates a UNIQUE index on sale_id, confirming that a unique column accepts the index and that equality lookups on that column use it.
-- UNIQUE index (separate read-only example)
DROP INDEX IF EXISTS ix_u_demo;
CREATE UNIQUE INDEX ix_u_demo ON perf_sales(sale_id);
EXPLAIN QUERY PLAN
SELECT sale_id FROM perf_sales WHERE sale_id = 12345;
-- Trying to add UNIQUE on a column with duplicates is rejected
-- CREATE UNIQUE INDEX ix_bad ON perf_sales(product);
--> UNIQUE constraint failed: perf_sales.product
-- product has 200 distinct values repeated across 50,000 rows, so it can't be made UNIQUE
Tips — when to add an index
Rule of thumb: an index tends to beat a SCAN when you can narrow the result to about 15-20% or less of the table. Above that threshold, the optimizer often picks SCAN automatically anyway.
- Add one: equality or range searches on a column of a large table, primary keys / foreign keys / UNIQUE columns, JOIN keys, columns referenced by ORDER BY / GROUP BY
- Skip it: small tables with a few thousand rows or fewer, low-selectivity columns like gender, write-heavy tables, LIKE '%foo%' (a leading wildcard can't use an index)
The baseline workflow: check the SCAN / SEARCH plan with EXPLAIN QUERY PLAN and add indexes only to the slow queries.
Knowledge Check
Answer each question one by one.
Q2Which query benefits most from the composite index CREATE INDEX ix ON perf_sales(emp_id, sale_date);?
Q3What happens when you run CREATE UNIQUE INDEX on the product column, which contains duplicates?