A table scan is the database version of reading every page of a book when you only needed one paragraph.
Question: What is a table scan in SQL?
Answer: A table scan means the database reads every row, or every data page, in a table to find matching rows. It usually happens when there is no useful index, the filter matches many rows, or the predicate is written in a way the optimizer cannot use an index. It is not always bad: for small tables or broad queries, scanning can be faster than doing many random index lookups.
Interview-Ready Answer: I’d say a table scan is when the engine checks the whole table instead of jumping to rows through an index. That usually means either the index is missing, the condition is not selective, or the query makes the column hard to search, like wrapping it in a function. It is not automatically a problem, because for small tables or queries that need a big chunk of the data, a sequential scan can be the cheapest plan.
Detailed Explanation:
A table scan is a sequential read of the table’s data pages. Different engines label the same idea differently: PostgreSQL often shows Seq Scan, SQL Server may show Table Scan or Clustered Index Scan, and MySQL often uses ALL in the plan. The key idea is simple: the engine starts at one end and checks row by row instead of jumping straight to matching rows through an index.
WHERE clause.A useful mental model is this: the optimizer is choosing between read a lot in order and do many targeted jumps. Sequential I/O means nearby pages are read together, which is friendly to disks and SSDs. If a row averages about 200 bytes and the page size is 8 KB, one page holds roughly 40 rows; a million-row table can therefore mean tens of thousands of page reads.
DATE(created_at) or LOWER(email) can hide the raw column from a normal index.One important term is sargable, which means the predicate can use an index efficiently. A predicate like created_at >= '2026-01-01' is usually sargable, but DATE(created_at) = '2026-01-01' often is not unless you build an expression index. A covering index is another helpful term: it contains every column the query needs, so the engine can answer from the index alone without touching the table.
| Plan | What it does | Best for | Typical Cost |
|---|---|---|---|
| Table scan | Reads all table pages | Small tables, broad filters | O(N) |
| Index scan | Walks all leaf pages of an index | Large queries on one index | O(N) over index |
| Index seek | Jumps to matching key range | Highly selective filters | O(log N + k) |
That table shows the main trade-off: a scan is not automatically bad. If you need 30% to 80% of the rows, a scan can be faster than many random index lookups. On the other hand, if you only need a few rows out of millions, a seek is usually much better. In many engines, a scan is also easy to parallelize across multiple workers for very large tables.
Time complexity for the scan itself is roughly O(N), because the engine checks each row or page once. Extra space is usually O(1) for the scan operator itself, aside from normal engine caches and whatever operators come before or after it. On a warm cache, a scan may become CPU-bound instead of disk-bound; on a cold cache, it can be mostly about I/O.
Common edge cases that force scans include LIKE '%term' with a leading wildcard, arithmetic on the column such as amount + 0, and boolean or status columns with very low cardinality, where an index may not help much. The exact plan names and thresholds differ by engine and version, but the idea does not change: the optimizer picks the cheapest path based on statistics, row estimates, and the amount of data it expects to return.
Real-World Example: In an e-commerce checkout service, a new reporting endpoint filtered millions of orders by day using DATE(created_at). That looked harmless, but it forced a scan because the engine could not use the normal index on created_at. The symptom was ugly: the database disk queue went high, p95 latency jumped from under 200 ms to several seconds, and users saw the dashboard spin or checkout requests time out during peak traffic.
The incident showed up in logs as slow queries with plans like Seq Scan or Table Scan, plus CPU that was not even the main bottleneck; storage was. The fix was to rewrite the filter as a date range on the raw column and add the right index. After that, the engine could jump to the day’s rows instead of reading the whole table, and the service recovered immediately.
-- Demonstration: show how a table scan can appear, then how an index changes the plan.
-- SQLite syntax is used because it is runnable in a small self-contained script.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
amount INTEGER NOT NULL,
status TEXT NOT NULL
);
-- Make the table large enough that the planner has a reason to care about access paths.
WITH RECURSIVE seq(n) AS (
VALUES(1)
UNION ALL
SELECT n + 1
FROM seq
WHERE n < 1000
)
INSERT INTO orders (id, customer_id, created_at, amount, status)
SELECT
n,
(n % 50) + 1,
CASE WHEN n <= 500 THEN '2026-01-01' ELSE '2026-01-02' END,
n % 200,
CASE WHEN n % 3 = 0 THEN 'paid' ELSE 'open' END
FROM seq;
-- No index yet: this filter usually has to inspect many rows, so a scan is the natural plan.
EXPLAIN QUERY PLAN
SELECT id, customer_id, amount
FROM orders
WHERE amount = 150;
CREATE INDEX idx_orders_amount ON orders(amount);
-- With the index in place, the same predicate can use a search instead of reading every row.
EXPLAIN QUERY PLAN
SELECT id, customer_id, amount
FROM orders
WHERE amount = 150;
-- Edge case: wrapping the indexed column in an expression hides it from the normal index.
-- This is a common reason a query still scans even after you add an index.
EXPLAIN QUERY PLAN
SELECT id, customer_id, amount
FROM orders
WHERE amount + 0 = 150;
-- Range predicates can also use the index when the column stays bare and searchable.
EXPLAIN QUERY PLAN
SELECT id, customer_id, amount
FROM orders
WHERE amount BETWEEN 140 AND 145;Follow-up & Tricky Questions:
EXPLAIN or EXPLAIN ANALYZE and look for labels like Seq Scan, Table Scan, Clustered Index Scan, or ALL. Then compare estimated rows to actual rows to see whether the plan choice was reasonable.SELECT * cause a table scan? Not by itself. The scan decision comes from the filter and the data distribution; SELECT * mainly affects how much data is returned, not whether the engine can search efficiently.Common Mistakes:
Memory Hook: Think of the database as choosing between reading the whole phone book and using the index at the back. If you need one name, use the index; if you need most of the book, just read it cover to cover.
Cheat Sheet:
EXPLAIN to confirm the real plan instead of guessing.Practice Tasks:
EXPLAIN QUERY PLAN before and after adding an index.DATE(column) or LOWER(column) so the column stays searchable.