SQL Server Internals — How the Database Engine Really Works
In short: A clustered index defines the physical order of the table: its B+ tree leaf level contains the actual data rows, so there can be only one.
- A nonclustered index is a separate B+ tree whose leaf rows contain the index key plus a pointer (the clustering key or RID) to the data row; there can be many.
- Queries needing columns not in a nonclustered index do key lookups into the clustered index.
In short: A page is the basic 8 KB unit of storage and I/O, holding rows (≈8,060 bytes max in-row data) with a header and a row offset array.
- An extent is 8 contiguous pages (64 KB); SQL Server allocates space in extents.
- Special pages (GAM, SGAM, PFS, IAM) track which extents and pages are free or belong to which object.
In short: READ UNCOMMITTED (dirty reads possible), READ COMMITTED (default;
- no dirty reads), REPEATABLE READ (rows you read can't change), SERIALIZABLE (no phantoms — range locks) plus row-versioning options READ COMMITTED SNAPSHOT (statement-level consistent versions, readers don't block writers) and SNAPSHOT (transaction-level consistent snapshot with update-conflict detection).
In short: The step-by-step strategy the optimizer chose to run a query (operators like seeks, scans, joins, sorts).
- Read it right to left following the data flow, check the most expensive operators, compare estimated vs actual row counts, and look for scans on large tables, key lookups repeated many times, sorts/hash spills, implicit conversion and missing-index warnings.
- Use
SET STATISTICS IOto measure logical reads before and after a change.
Connection and TDS packets → parser (syntax) → algebrizer (binding names and types) → plan cache lookup → cost-based optimizer using statistics to choose a plan (cached) → executor runs the operators → storage engine reads pages via the buffer pool (from disk if not cached), taking locks or reading row versions per isolation level → rows streamed back to the client.
In short: Objects with a histogram (up to 200 steps) and density information about column value distributions, used by the optimizer to estimate how many rows each operation returns.
- Good estimates → good plans (right join type, memory grant, index choice).
- Stale or missing statistics lead to bad plans; they're auto-created/updated, and can be updated manually (`UPDATE STATISTICS ...
- WITH FULLSCAN`) after large data changes.
In short: SQL Server compiles a parameterised query or procedure using the parameter values of the first execution and reuses that plan for all later values.
- If data is skewed, the plan can be terrible for other values.
- Fixes: a covering index that suits all values,
OPTION (RECOMPILE),OPTIMIZE FOR, splitting logic, forcing a good plan with Query Store, or SQL Server 2022's Parameter Sensitive Plan optimisation.
In short: Every modification is written to the transaction log (with an LSN) before the data page changes reach disk, and a commit waits until its log records are hardened.
- After a crash, recovery runs analysis (find dirty pages and open transactions), redo (replay logged changes since the last checkpoint) and undo (roll back uncommitted transactions), returning the database to a consistent state.
In short: A session holding locks (usually a long or open transaction, or one scanning many rows because of missing indexes) blocks others needing incompatible locks.
- Find it with
sys.dm_exec_requests(blocking_session_id),sp_WhoIsActive, and the head blocker's query/plan; - fix by shortening transactions, adding indexes, committing promptly from the app, and enabling RCSI so readers don't block writers.
In short: When a nonclustered index finds rows but the query needs columns not in that index, SQL Server looks up each row in the clustered index (key lookup) or heap (RID lookup).
- For many rows it's very expensive.
- Remove it by adding the needed columns to the index with
INCLUDE(covering index), or selecting fewer columns.
In short: The main memory area where SQL Server caches 8 KB data and index pages.
- Queries read pages from the buffer pool (logical reads); if a page isn't there it's read from disk first (physical read).
- Modified (dirty) pages are written back by checkpoint and the lazy writer.
- Set max server memory so the buffer pool doesn't starve the OS.
In short: Locks protect logical data consistency between transactions (rows, pages, tables), are held according to isolation level (possibly until commit) and are visible in sys.dm_tran_locks.
- Latches are lightweight, short-term internal synchronisation for in-memory structures like buffer pages and aren't controlled by isolation levels.
- Latch contention (
PAGELATCH_*) appears with hot pages such as the last page of an identity key under heavy inserts.
In short: READ UNCOMMITTED doesn't take shared locks, so a query can read uncommitted data that later rolls back, read the same row twice or miss rows when pages split during an allocation-order scan, and still be blocked by schema locks.
- It's "fast but wrong".
- Use RCSI or SNAPSHOT for non-blocking consistent reads instead.
In short: For analytical queries scanning and aggregating millions of rows — data warehouse fact tables (clustered columnstore) or real-time reporting on OLTP tables (nonclustered columnstore).
- Column-wise compressed storage and batch-mode execution dramatically reduce I/O and CPU.
- Avoid as the only index for workloads of single-row lookups and frequent small updates.
In short: Log space can't be reused while something still needs it: in FULL/BULK_LOGGED recovery without regular log backups, long-running open transactions, replication or availability-group replicas behind, CDC.
- Check
log_reuse_wait_descinsys.databases; schedule log backups, find and end long transactions, and avoid shrinking repeatedly (causes VLF fragmentation).
In short: Check Query Store for a plan change (regressed query) — often due to a statistics update, parameter sniffing with a different first value, or an index change.
- Compare old and new plans (estimated vs actual rows, seek vs scan, join types).
- Quick fix: force the good plan in Query Store; real fix: update statistics, add/adjust a covering index, or rewrite non-SARGable predicates.
- Also rule out blocking (
LCK_M_*waits) and resource pressure (waits, memory, I/O).
In short: Capture the deadlock graph from the system_health Extended Events session, identify the statements, objects and lock types.
- Typical fixes: access tables in a consistent order, keep transactions short, add indexes so fewer rows are locked (especially on foreign keys and WHERE columns), use
UPDLOCKfor read-then-write, enable RCSI to remove reader/writer conflicts — and add retry logic for error 1205 in the application.
In short: All inserts go to the last page of the B-tree, so many threads compete for the latch on that single hot page.
- Options:
OPTIMIZE_FOR_SEQUENTIAL_KEY = ON(2019+), a hash-partitioned table, a different clustering key spreading inserts (with trade-offs like page splits), In-Memory OLTP for extreme rates, or batching inserts.