What Is a Page Split in SQL Server?

Written by

in

A page split in SQL Server happens when an inserted or updated row no longer fits in its 8 KB data page. SQL Server allocates a new page, moves about half the rows to it, and links the pages in index order. End-of-index splits are cheap; mid-index splits fragment pages and add I/O.

What a page split is

SQL Server stores rows on 8 KB data pages, as described in the pages and extents architecture guide. A page split is the operation that runs when an insert or update needs to place a row on a page that has no room for it.

The engine allocates a new page, moves roughly half of the existing rows across, and rewrites the page pointers so the B-tree keeps its logical key order. The term applies at the leaf level of a clustered index and at the leaf level of a nonclustered index, but it can also affect an intermediate index page when the page chain must grow.

A data page is the fixed-size unit that SQL Server reads and writes as a whole; an index page stores keys and child-page pointers for navigation. After a split, the parent index page stores an extra pointer to the new page. A split is normal B-tree growth. It becomes a cost only when it happens frequently in the middle of an ordered page range, or when the table is large enough for the extra I/O and transaction log writes to show up in wait statistics.

How a page split happens

The cause of a page split is a single condition: SQL Server must place a row on a specific page, and that page cannot hold the whole row. The common SQL Server page split causes follow the same sequence:

  1. An INSERT or UPDATE identifies the target page from the clustered key or nonclustered key.
  2. SQL Server checks the free space on that 8 KB data page. Under the default fill factor of 100, pages are filled to capacity and leave little room for new rows.
  3. If the row does not fit, SQL Server allocates a new page and moves about half the rows from the original page.
  4. It updates the page pointers and records the page movement in the transaction log.

Where the split lands matters. An IDENTITY or sequence key inserted at the end of the sorted key range produces an append split, so it is cheap. A non-sequential GUID inserted between existing keys produces a mid-page split, leaving free space on both pages and creating fragmentation. An UPDATE that grows a variable-length column follows the same path; Extended Events records it as SPLIT_FOR_UPDATE.

Why page splits matter

A page split is not just a logical bookkeeping change. It has physical costs:

  • I/O: SQL Server writes the original page, the new page, and often the parent index page. Splits therefore multiply the writes of a single insert or update.
  • Transaction log: the row movement is logged; frequent splits increase log generation and can slow log backups.
  • Index fragmentation: mid-page splits leave roughly half-empty pages. avg_fragmentation_in_percent in sys.dm_db_index_physical_stats rises as pages become out of order.
  • Buffer pool pressure: more pages must be read for the same number of rows, which can lower cache efficiency.

Not all splits are equal. An append split at the right-hand edge of a clustered index moves no existing rows and leaves the original page full, so its main cost is allocating a page and logging the new page chain. A mid-page split moves half the rows and turns one full page into two half-full pages; that is the pattern that shows up later as fragmentation.

The fill factor documentation shows how a split moves rows and how free space on a page delays the next split.

How to detect page splits in SQL Server with DMVs

The quick way to quantify splits without setting up Extended Events is to query sys.dm_db_index_operational_stats. The leaf_allocation_count column counts page allocations at the leaf level and is a direct signal of page splits; compare it with leaf_insert_count and leaf_update_count to see whether most inserts reach an empty page or force a split.

SELECT
    DB_NAME(ios.database_id) AS database_name,
    OBJECT_NAME(ios.object_id) AS table_name,
    i.name AS index_name,
    ios.leaf_insert_count,
    ios.leaf_update_count,
    ios.leaf_allocation_count
FROM sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) AS ios
JOIN sys.indexes AS i
    ON i.object_id = ios.object_id
   AND i.index_id = ios.index_id
WHERE i.index_id > 0
ORDER BY ios.leaf_allocation_count DESC;

Run it in the target database; the function takes a database ID and returns one row per index. A high leaf_allocation_count compared with leaf_insert_count points to frequent splits. leaf_update_count is useful when updates, not inserts, are the likely trigger. Expect leaf_allocation_count to stay close to leaf_insert_count when inserts land at the end of an increasing key. With a non-sequential GUID, the allocation count normally climbs faster than the insert count. If you only want one table, join to sys.partitions or filter by OBJECT_ID.

These counters are cumulative since the service last started, so take two snapshots and compare the difference across a known workload. After a split-heavy period, check avg_fragmentation_in_percent in sys.dm_db_index_physical_stats; page splits are one cause, but not the only one. Column definitions are documented in sys.dm_db_index_operational_stats (Transact-SQL).

Tracking page splits with Extended Events

Extended Events exposes the sqlserver.page_split event for cases where you need to attribute a split to a specific query or session. The event includes a splitOperation field with two values: SPLIT_FOR_INSERT, for a split caused by an insert, and SPLIT_FOR_UPDATE, for a split caused by an update that grows the row on its current page. Create an event session on the busy database, add actions for sql_text or session_id if you need to find the source, and keep the session short. Extended Events capture only what happens while the session runs; the DMV counters above are cumulative and need less setup.

SQL Server fill factor and page splits

Fill factor sets how full SQL Server makes each index page when it rebuilds or creates an index. The default is 100, which leaves no free space for later inserts. A fill factor of 80 leaves 20 percent free, so more rows can be added before a split is needed. That reduces mid-page splits for indexes that receive random inserts, such as a non-sequential GUID key.

The trade-off is storage and cache: pages are less full, so the same index uses more space and more buffer pool. A fill factor of 80 is not a global fix; it helps workloads that insert into random key values, and it does little for append-only inserts, which split at the leaf edge anyway. Set the fill factor at build or rebuild time, and test it on a copy first. Microsoft Learn’s fill factor guide documents the setting and its effect on page splits.

When to worry about page splits

Context matters more than the raw split count. Use this table as a starting point before you change fill factor or rebuild an index.

ScenarioUsually fineInvestigate
Sequential clustered key, such as an IDENTITY columnAppend splits at the end of the leaf level; pages stay full and little fragmentation appearsIf insert volume is so high that log writes or latch contention become visible
Non-sequential GUID as the clustered keySmall, rarely written tables can tolerate the occasional splitFrequent mid-page splits on large tables, rising leaf_allocation_count
Updates that grow variable-length columnsA fill factor below 100 leaves room for longer rowsRepeated SPLIT_FOR_UPDATE events, extra I/O, growing avg_fragmentation_in_percent
Read-mostly table with low insert volumePage splits are rare and usually not worth tuningIf a single batch triggers a large number of splits and then queries slow down

FAQ

Is a page split bad in SQL Server?

Not always; append splits at the end of an increasing key are cheap, while mid-index splits fragment pages.

How do page splits affect index fragmentation?

They move rows to a new page, leaving free space and out-of-order pages, which raises avg_fragmentation_in_percent.

How does fill factor reduce page splits?

Fill factor below 100 leaves free space on each page so more rows fit before a split is needed.

How do you track page splits in SQL?

Use Extended Events with sqlserver.page_split, or DMVs like sys.dm_db_index_operational_stats.

Does a page split happen on insert or update?

Both; an update that grows a row can trigger SPLIT_FOR_UPDATE when the row no longer fits.

Sources

  • Microsoft Learn, “Pages and Extents Architecture Guide” — page structure and 8 KB data pages.
  • Microsoft Learn, “Specify Fill Factor for an Index” — fill factor and page split behaviour.
  • Microsoft Learn, “sys.dm_db_index_operational_stats (Transact-SQL)” — DMV columns used in the detection script.