Introduction
Page splits are one of those silent performance killers in SQL Server that many DBAs have heard about but rarely analyze in depth. On the surface, inserts might look harmless – rows keep getting added, queries return results, and no errors appear. But underneath, SQL Server is constantly managing how data is physically stored on 8KB pages, and each insert has the potential to fragment your index and degrade performance over time.
Inserts at the end of a table generally work smoothly. SQL Server simply appends the new row to the current last page, or allocates a brand new page if there’s no space left. However, when you insert rows out of order into a clustered index, SQL Server must maintain logical order. It does this by splitting existing pages – taking about half the rows from the full page and moving them to a newly allocated page – to create space for the inserted value.
Page splits aren’t just an academic curiosity. They directly impact IO, consume CPU cycles to reorganize data, and can cause blocking or concurrency issues under heavy workloads. They also lead to index fragmentation, slowing down scans and seeks if not managed properly.
SQL Server provides performance counters such as Page Splits/sec and Page Allocations/sec to monitor these activities. While these counters are useful for understanding overall split rates and allocations, they don’t reveal what’s really happening at the page level. A low-level page forensic approach can show exactly which pages were split and how their structure changed. This deeper insight is especially valuable in edge cases – I have seen page splits occur even during serial inserts where they typically aren’t expected, and only detailed page analysis can uncover such differences.
That’s where page forensics come in. By inspecting individual page headers and linkage details, we can see precisely how SQL Server reorganized pages during inserts. In my own investigations, I’ve even found page splits occurring in some serial inserts where they weren’t expected – something aggregate counters alone could never highlight.
In this article, I’ll create a wide-column table to amplify page split effects. We will insert rows in order to observe normal page allocations, then insert rows out of order to force mid-page splits, and finally use page forensics to uncover what really changed under the hood – revealing insights that counters alone simply cannot provide.
Page Splits vs New Page Allocations
SQL Server exposes performance counters such as Page Splits/sec and Page Allocations/sec under the Access Methods object in sys.dm_os_performance_counters. While these counters give an overall rate, they don't differentiate the nature of page activity. For example, new page allocations are harmless – they just add a fresh page at the end. But page splits indicate that existing pages were split due to mid-table inserts or updates causing row expansions.
In this article, we go deeper than these aggregate counters. Using low-level page forensics, we will decode exactly how pages were allocated, split, and linked before and after inserts, showing differences between serial inserts (at the end) and out-of-order inserts (in the middle).
Page Forensics: What Are We Really Looking At?
To understand what happens during inserts, think like the database engine. SQL Server stores rows in 8KB data pages, linked together in a doubly linked list within the B-tree structure of an index. Each page header has critical metadata fields:
m_pageId – page’s file and page number, e.g. (1:1500)
m_prevPage and m_nextPage – pointers to previous and next pages in the chain
m_slotCnt – number of slots or rows currently on the page
m_freeCnt – free space remaining on the page in bytes
m_type – type of page (1 for data page, 2 for index page ,10 for IAM etc..)
These can be examined using DBCC PAGE, for example:
DBCC TRACEON(3604); GO DBCC PAGE ('simple_talk', 1, 1500, 3); GO
Here, 'simple_talk'
is the database name, 1
is the file ID, and 1500
is the page ID. The fourth parameter (3
) controls output detail level. The WITH TABLERESULTS, NO_INFOMSGS options can also be used to extract output in query-friendly formats for forensic scripts.
By analyzing these fields before and after inserts, we see whether rows were simply appended to new pages, or if SQL Server split existing pages to maintain order. This approach goes far beyond high-level counters, letting us pinpoint specific page chains affected by different insert patterns.
Capturing Page Forensics
I prepared below script to capture page-level forensic data in SQL Server. First, it turns on DBCC TRACEON(3604) to enable output to the console. It sets up your database and table context, then pulls all pages allocated to the target table using sys.dm_db_database_page_allocations, filtering for data and index pages only.
For each page, it runs DBCC PAGE with the TABLERESULTS option to read its detailed structure into a temporary table. It extracts key fields: page ID, slot count (number of rows or entries), free space, next page, and previous page pointers. These details reveal how a page is linked and how full it is, which are crucial for detecting page splits, fragmentation, or corruption.
The script then creates a combined string from these extracted values and generates a SHA-512 hash, allowing easy tracking of any changes in page content over time. Additionally, it reads the full page dump file from disk (using OPENROWSET) to compute a second hash representing the entire physical file, verifying data integrity at the file level.
Finally, it inserts all this into the PageTracking table: table name, page metadata, computed hashes, and the raw dump. This creates a robust forensic snapshot of page state, helping DBAs analyse internal changes, detect silent page splits, or validate data corruption investigations. In short, it is like taking a time-stamped MRI scan of every data page in your SQL Server table for deep internals analysis.
We will execute this script after each insert to capture the page dump once new rows are added in our lab environment. Throughout this tutorial, we will refer to it as the Page Forensic Tracker Script.
USE [simple_talk] GO /****** Object: Table [dbo].[PageTracking] Script Date: 7/1/2025 11:48:59 PM ******/SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[PageTracking]( [TableName] [sysname] NOT NULL, [FileID] [int] NULL, [PageID] [int] NULL, [CaptureTime] [datetime2](7) NULL, [PageHash] [varbinary](64) NULL, [PageDump] [nvarchar](max) NULL, [UsedBytes] [int] NULL, [DumpFileHash] [varbinary](64) NULL, [m_pageId] [nvarchar](100) NULL, [m_slotCnt] [int] NULL, [m_freeCnt] [int] NULL, [m_nextPage] [nvarchar](100) NULL, [m_prevPage] [nvarchar](100) NULL, [CombinedHash] [varbinary](64) NULL, [m_type] [nvarchar](100) NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE [dbo].[PageTracking] ADD DEFAULT (sysdatetime()) FOR [CaptureTime] GO -- Enable DBCC output to console DBCC TRACEON(3604); GO -- Setup DECLARE @dbid INT = DB_ID('simple_talk'); DECLARE @objectid INT = OBJECT_ID('dbo.test_page_splits_and_allocations'); IF OBJECT_ID('tempdb..#PageList') IS NOT NULL DROP TABLE #PageList; SELECT allocated_page_file_id AS FileID, allocated_page_page_id AS PageID, page_type_desc INTO #PageList FROM sys.dm_db_database_page_allocations(@dbid, @objectid, NULL, NULL, 'DETAILED'); DECLARE @FileID INT, @PageID INT, @PageType NVARCHAR(100); DECLARE PageCursor CURSOR FOR SELECT FileID, PageID, page_type_desc FROM #PageList; OPEN PageCursor; FETCH NEXT FROM PageCursor INTO @FileID, @PageID, @PageType; WHILE @@FETCH_STATUS = 0 BEGIN IF @PageType LIKE '%DATA%' OR @PageType LIKE '%INDEX%' BEGIN DECLARE @PageDump TABLE ([ParentObject] NVARCHAR(MAX), [Object] NVARCHAR(MAX), [Field] NVARCHAR(MAX), [Value] NVARCHAR(MAX)); DECLARE @sql NVARCHAR(MAX) = 'DBCC PAGE (' + CAST(@dbid AS VARCHAR) + ', ' + CAST(@FileID AS VARCHAR) + ', ' + CAST(@PageID AS VARCHAR) + ', 3) WITH TABLERESULTS, NO_INFOMSGS'; BEGIN TRY INSERT INTO @PageDump ([ParentObject], [Object], [Field], [Value]) EXEC sp_executesql @sql; -- Extract required fields DECLARE @m_pageId NVARCHAR(100) = NULL, @m_slotCnt INT = NULL, @m_freeCnt INT = NULL, @m_nextPage NVARCHAR(100) = NULL, @m_prevPage NVARCHAR(100) = NULL; SELECT @m_pageId = [Value] FROM @PageDump WHERE [Field] LIKE '%m_pageId%'; SELECT @m_slotCnt = TRY_CAST([Value] AS INT) FROM @PageDump WHERE [Field] LIKE '%m_slotCnt%'; SELECT @m_freeCnt = TRY_CAST([Value] AS INT) FROM @PageDump WHERE [Field] LIKE '%m_freeCnt%'; SELECT @m_nextPage = [Value] FROM @PageDump WHERE [Field] LIKE '%m_nextPage%'; SELECT @m_prevPage = [Value] FROM @PageDump WHERE [Field] LIKE '%m_prevPage%'; -- Create combined string for hashing DECLARE @CombinedString NVARCHAR(MAX) = ISNULL(@m_pageId,'') + '|' + ISNULL(CAST(@m_slotCnt AS NVARCHAR),'') + '|' + ISNULL(CAST(@m_freeCnt AS NVARCHAR),'') + '|' + ISNULL(@m_nextPage,'') + '|' + ISNULL(@m_prevPage,''); DECLARE @CombinedHash VARBINARY(64) = HASHBYTES('SHA2_512', @CombinedString); -- Read file dump for file hash DECLARE @FilePath NVARCHAR(4000) = 'C:\Data\Page_Dumps\test_page_splits_and_allocations_' + CAST(@FileID AS NVARCHAR) + '_' + CAST(@PageID AS NVARCHAR) + '.txt'; DECLARE @FileContent VARBINARY(MAX) = NULL; DECLARE @FileSql NVARCHAR(MAX) = N'SELECT @FileContentOut = BulkColumn FROM OPENROWSET(BULK ''' + @FilePath + ''', SINGLE_BLOB) AS x;'; BEGIN TRY EXEC sp_executesql @FileSql, N'@FileContentOut VARBINARY(MAX) OUTPUT', @FileContentOut = @FileContent OUTPUT; PRINT 'Read file: ' + @FilePath + ' Size: ' + CAST(DATADATADATADATADATADATADATADATADATADATADATADATADATADATALENGTH(@FileContent) AS NVARCHAR(100)); END TRY BEGIN CATCH PRINT 'OPENROWSET failed for ' + @FilePath; SET @FileContent = NULL; END CATCH; DECLARE @FileHash VARBINARY(64) = NULL; IF @FileContent IS NOT NULL SET @FileHash = HASHBYTES('SHA2_512', @FileContent); -- Insert into tracking table INSERT INTO dbo.PageTracking ( TableName, FileID, PageID, CaptureTime, m_pageId, m_slotCnt, m_freeCnt, m_nextPage, m_prevPage, CombinedHash, PageDump, DumpFileHash ) VALUES ( 'test_page_splits_and_allocations', @FileID, @PageID, SYSDATETIME(), @m_pageId, @m_slotCnt, @m_freeCnt, @m_nextPage, @m_prevPage, @CombinedHash, @FileContent, @FileHash ); END TRY BEGIN CATCH PRINT 'Skipping incompatible page: FileID ' + CAST(@FileID AS VARCHAR) + ', PageID ' + CAST(@PageID AS VARCHAR); END CATCH END FETCH NEXT FROM PageCursor INTO @FileID, @PageID, @PageType; END CLOSE PageCursor; DEALLOCATE PageCursor; GO
Simulating the Lab
I created a table with wide fixed-length columns to ensure each row consumes substantial space, making splits easy to observe.
Using CHAR(1000)
ensures each row is around 3KB, resulting in only two or three rows per 8KB page.
Serial Inserts (In-Order)
I inserted 20 rows with ascending Ids to observe how SQL Server allocates pages when data is inserted in order. This is a script which is always having ID in increasing order and so it does cause no page split or minimal page split.
This script will insert rows with ID 10,20,20.... till 200.
After above insert run the page forensic tracker script.
Next we will run another serial inserts , but this time id will be 210 , 220... 400, and we will execute page forensic tracker script again to get snapshot after both inserts and compare how page structures get changed after later insert.
This is worth to note insert is still serial and page splits will be minimal in this case.
DECLARE @i INT = 210; WHILE @i <= 400 BEGIN INSERT INTO dbo.test_page_splits_and_allocations (Id, Data1, Data2, Data3) VALUES (@i, REPLICATE('A',1000), REPLICATE('B',1000), REPLICATE('C',1000)); SET @i += 10; END
After running both inserts lets query PageTracking table to get snapshot of all pages after first and second serial inserts and also new pages allocated while second insert.
SELECT [TableName] ,[FileID] ,[PageID] ,[CaptureTime] ,[m_pageId] ,[m_slotCnt] ,[m_freeCnt] ,[m_nextPage] ,[m_prevPage] ,[CombinedHash] ,[m_type] FROM [dbo].[PageTracking] order by 1,2,3,4 GO
We get below output in tabular format after running above query(Scroll to complete right for better view, also it is more interpretable if you copy it to excel).
TableName | FileID | PageID | CaptureTime | m_pageId | m_slotCnt | m_freeCnt | m_nextPage | m_prevPage | CombinedHash | m_type |
test_page_splits_and_allocations | 1 | 1735636 | 2025-07-01 22:49:45 | (1:1735636) | 2 | 6 | (0:0) | (0:0) | 0x4478E108CE69A86DC99F88370E59540E592AEFEB4B44A4B41DFDECABFBCBA590F8F50754C1596F96E5536EB64DD2D9EA3EAAE37CAC66E9CAD42E3B13DBCF4261 | 10 |
test_page_splits_and_allocations | 1 | 1735636 | 2025-07-01 22:50:43 | (1:1735636) | 2 | 6 | (0:0) | (0:0) | 0x4478E108CE69A86DC99F88370E59540E592AEFEB4B44A4B41DFDECABFBCBA590F8F50754C1596F96E5536EB64DD2D9EA3EAAE37CAC66E9CAD42E3B13DBCF4261 | 10 |
test_page_splits_and_allocations | 1 | 2134024 | 2025-07-01 22:49:45 | (1:2134024) | 2 | 2070 | (1:2134028) | (0:0) | 0x046933607CC80DE6A56D8355B37D63A7BE45EFEE3B2706C62E52A641208B0C7EB53295A3B12609BE37E00F9FBE00147716E912A616C51376F9F88CC54C607765 | 1 |
test_page_splits_and_allocations | 1 | 2134024 | 2025-07-01 22:50:43 | (1:2134024) | 2 | 2070 | (1:2134028) | (0:0) | 0x046933607CC80DE6A56D8355B37D63A7BE45EFEE3B2706C62E52A641208B0C7EB53295A3B12609BE37E00F9FBE00147716E912A616C51376F9F88CC54C607765 | 1 |
test_page_splits_and_allocations | 1 | 2134025 | 2025-07-01 22:49:45 | (1:2134025) | 4 | 8044 | (0:0) | (0:0) | 0x4DC7CCCE858D0A3373C5AC4EBF970FFB4DF838FC959D2AB13DE82596C41A40658A7421BB4E9830CD2EC6EF9D859A1909EF73B52F481E6CA6B65045FD0B21AAEB | 2 |
test_page_splits_and_allocations | 1 | 2134025 | 2025-07-01 22:50:44 | (1:2134025) | 14 | 7914 | (0:0) | (0:0) | 0xC51A3F99D9FBB870B658DB99B860B542487DFD35FDD5ACD57C0F9A921D8CCF3F40B3D2BD85C381DD91B46F9753C8E224E777205480585E87DD27E793A14B3B0F | 2 |
test_page_splits_and_allocations | 1 | 2134026 | 2025-07-01 22:49:45 | (1:2134026) | 1 | 5083 | (0:0) | (1:2134027) | 0xF5A1A47AFED6877544D91F7F8A97BC67563832F8591184B994E7178A89F8F6667BF7FA19463E74247485B3DD520B9419E318BC51134409A86C545ED890563053 | 1 |
test_page_splits_and_allocations | 1 | 2134026 | 2025-07-01 22:50:44 | (1:2134026) | 2 | 2070 | (1:2134029) | (1:2134027) | 0x89659601FF477DBAD14F89E92BA991FA60C71CCB7CF0E988E76EA6EEFC212770D7E73FCFFE758E164919697CBC685ABADC17A8FE7BCE8DEFBA25D0BBAB3463D1 | 1 |
test_page_splits_and_allocations | 1 | 2134027 | 2025-07-01 22:49:45 | (1:2134027) | 1 | 5083 | (1:2134026) | (1:2134028) | 0x57D264DBF449342FC5073C99B129F69DC96DE0ADEBCC5F24A08348BD4BAB5271598E4C741826D41ACB79317FA7309ABA822D5BF69402D281323ED6322D3BA784 | 1 |
test_page_splits_and_allocations | 1 | 2134027 | 2025-07-01 22:50:44 | (1:2134027) | 1 | 5083 | (1:2134026) | (1:2134028) | 0x57D264DBF449342FC5073C99B129F69DC96DE0ADEBCC5F24A08348BD4BAB5271598E4C741826D41ACB79317FA7309ABA822D5BF69402D281323ED6322D3BA784 | 1 |
test_page_splits_and_allocations | 1 | 2134028 | 2025-07-01 22:49:45 | (1:2134028) | 1 | 5083 | (1:2134027) | (1:2134024) | 0x4FE7A78A2B77F6AF19712599DDC10DEAC65E7101BEA44B0C3D8E62CEFB3E92EC85C6A908C02D37F1D8CC4E4BC840B76BB013C362C71E934732544517EB756BCD | 1 |
test_page_splits_and_allocations | 1 | 2134028 | 2025-07-01 22:50:44 | (1:2134028) | 1 | 5083 | (1:2134027) | (1:2134024) | 0x4FE7A78A2B77F6AF19712599DDC10DEAC65E7101BEA44B0C3D8E62CEFB3E92EC85C6A908C02D37F1D8CC4E4BC840B76BB013C362C71E934732544517EB756BCD | 1 |
test_page_splits_and_allocations | 1 | 2134029 | 2025-07-01 22:49:45 | (1:2134029) | 1 | 8063 | (0:0) | (1:2044936) | 0x9E097F98EFE72471D1162E984471487C1358B248FE05FBFFDB5AB157D0675AE52014057F94A27BBDF79599439F03253D42F607DF8C34A24AAC22AB0CF746FD52 | 1 |
test_page_splits_and_allocations | 1 | 2134029 | 2025-07-01 22:50:44 | (1:2134029) | 2 | 2070 | (1:2134030) | (1:2134026) | 0x81DD7E21545AB637836BB9A763FB96E9E154E7358D898FD11FA1EEB454A2FF3D72B8D75C044E914034FA1CEBB52BADD44BDB8770B8AF88A9180BAFA8EE626327 | 1 |
test_page_splits_and_allocations | 1 | 2134030 | 2025-07-01 22:49:45 | (1:2134030) | 1 | 8063 | (0:0) | (1:2044936) | 0x982DD483E634B952F0BE3413037C8370DFB2D0634F14149E961D74E374258F8B0DF4D9BEC9F46C4144D5A3BB7C255CB729FEA42464C31DDF1D953B4A6C256136 | 1 |
test_page_splits_and_allocations | 1 | 2134030 | 2025-07-01 22:50:44 | (1:2134030) | 2 | 2070 | (1:2134031) | (1:2134029) | 0xC1F3E69BB77FA1DFA4AD8391F254E2BE472F3D8AD4D7F8BAC560CE3EE45012E62F9FBF24C4849FF5D9F404E72843B3AB6B3966775C97392ACF35E99BD5BF4B16 | 1 |
test_page_splits_and_allocations | 1 | 2134031 | 2025-07-01 22:49:45 | (1:2134031) | 1 | 8063 | (0:0) | (1:2044936) | 0xEFA3B2611D43702D2667D13037FA115471E58DBEE2D9824A9BA1853396E5DFA25EEE21ADB32707AEBE83995F077F84C7B5FDE04619670960B42EF86513030AA1 | 1 |
test_page_splits_and_allocations | 1 | 2134031 | 2025-07-01 22:50:44 | (1:2134031) | 2 | 2070 | (1:2142128) | (1:2134030) | 0xA786653485CD464E7F32E6B7BF15574CF4F13115E688786FD76B995C22BE84CDCF359FE146EC52ABCE29374379DE9ADF522B60F938EAEBC99A5A3749D2243331 | 1 |
test_page_splits_and_allocations | 1 | 2142128 | 2025-07-01 22:50:44 | (1:2142128) | 2 | 2070 | (1:2142129) | (1:2134031) | 0xF40DDD37116F66B32C466254A5BBDBD219B6CD30C40F2E515BE1323CBB4FADD106D5124AB5D2192B6C2DF13D1D8F27D0918B88580F53AE5E3924444B1632E849 | 1 |
test_page_splits_and_allocations | 1 | 2142129 | 2025-07-01 22:50:44 | (1:2142129) | 2 | 2070 | (1:2142130) | (1:2142128) | 0x4D0C4F741374EE7EBE505FD48AD971A04BEE874C81221400C5C036AC7D21ACC588B331792C12B2ACDA09D91E6316FF2D626F35889A5C0CF4A6474E51966A822D | 1 |
test_page_splits_and_allocations | 1 | 2142130 | 2025-07-01 22:50:44 | (1:2142130) | 2 | 2070 | (1:2142131) | (1:2142129) | 0x8D95E8FBD51EAD906EBAC5E179D524366E89A0DE20F71A1E7D2CCE4F1878B39A050ED7C5046EE15E7D507C472E7C42B557208F65A9EB57638550C6D2643FC4DD | 1 |
test_page_splits_and_allocations | 1 | 2142131 | 2025-07-01 22:50:44 | (1:2142131) | 2 | 2070 | (1:2142132) | (1:2142130) | 0xED962EC559C82FD8992B218B81FE5E69BA39B9CEEF8A8F7742F1D81F2EF897A313CF8722338E7FA02243DF694E87A93BA5954C99B7FAA38E946C6BA054127F1D | 1 |
test_page_splits_and_allocations | 1 | 2142132 | 2025-07-01 22:50:44 | (1:2142132) | 2 | 2070 | (1:2142133) | (1:2142131) | 0x38BAAD39C90E83B57C4DE2A44121D140730CA38E84A5793839EC2EED5AA6DD6EE06D022007B563D1DAAF513F6AC4178920230D7234E395B1CE1A71895D36BEA6 | 1 |
test_page_splits_and_allocations | 1 | 2142133 | 2025-07-01 22:50:44 | (1:2142133) | 2 | 2070 | (1:2142134) | (1:2142132) | 0x1C146BA8D4B014DCEC47548F9E75AF9DCCB5A3E90B1442E0074DF97F112E57FE505CAC9708E675023E79DE125C3675012BFC9618D35E72A66D952F7CDC659CF7 | 1 |
test_page_splits_and_allocations | 1 | 2142134 | 2025-07-01 22:50:44 | (1:2142134) | 1 | 5083 | (0:0) | (1:2142133) | 0xB6541C90F2494C742065B1E2F57DDE61B2669804A2EAFC040B1A9373B5857253FD15A908724745A9416BA26A0C978ED958AB385D8AEE4D96666B3776ED5F745E | 1 |
test_page_splits_and_allocations | 1 | 2142135 | 2025-07-01 22:50:44 | (1:2142135) | 1 | 8063 | (0:0) | (1:2044936) | 0x11706C5755826A8B63A1DA59A8D935D44CE00F60933997F970B3BED2C6C794B2B3A13CD580B0BE0B82BB0041FDE1E383C2A3AFF510F111DF02CB38E0652E96D3 | 1 |
Explaination(Table output After Serial Insert)
We have marked every page changed after second insertion in bold. m_type 1 refers to data pages, 2 refers to index pages and 10 refer to Index allocation Mapping pages. Lets focus on data pages which changed are (2134026,2134027, 2134028, 2134029, 2134030 and 2134031).
But lets focus the changes that occured in those pages:
Page 2134026
The m_freeCnt
reduced from 5083 to 2070, confirming new rows were inserted consuming space. Its m_nextPage
changed from (0:0) (no next page, it was previously a leaf data page) to (1:2134029). This indicates no page split occurred here – it had enough free space to accommodate the insert, and no row movement happened; only a new child page was linked.
Page 2134029
The m_freeCnt
decreased from 8063 to 2070, and m_slotCnt
rose from 1 to 2, showing new rows were inserted here. Its m_nextPage
changed from (0:0) to (1:2134030), and m_prevPage
updated from (1:2044936) to (1:2134026). This confirms no page split occurred as it had sufficient free space, and no row movement was needed; it simply accommodated the insert and updated links.
Page 2134030
Here, m_freeCnt
reduced from 8063 to 2070, and m_slotCnt
increased from 1 to 2, indicating a new row insertion. m_nextPage
updated from (0:0) to (1:2134031) and m_prevPage
changed from (1:2044936) to (1:2134029). Again, no split occurred, and no row movement took place; the page had enough space for direct insertion and linked to the next page.
Page 2134031
For this page, m_freeCnt
dropped from 8063 to 2070, and m_slotCnt
rose from 1 to 2, confirming a new row insertion. Its m_nextPage
changed from (0:0) to (1:2142128), and m_prevPage
updated from (1:2044936) to (1:2134030). There was no split as it had ample space, and no row movement was triggered – only a straightforward insert and page chain update occurred.
Few non changing pages are 2134027, 2134028. these page showed no change in m_freeCnt (remained 5083) or m_slotCnt (remained 1), meaning no new rows were inserted, no row movement occurred, and no split took place in this page.
But as we had expected, In serial insert, we were not able to find any page being splitted or their m_prevPage or m_nextPage changed from a real data page to another data page. If a m_prevPage or m_nextPage changes from (0,0) to some data page, it does just previously it has not parent or child item and just a new page has been allocated.
Mid-Table Inserts (Out-of-Order)
Next, I inserted five rows with Ids that fall between existing rows to force mid-table inserts and observe page split behaviour.
When inserting into the middle of the index, SQL Server must maintain logical order. This forces existing pages to split to accommodate the inserted rows, resulting in new pages and potentially fragmented allocation.
We ran page forensic tracker script before and after execution of above non-serial inserts. And after that we executed below script to check difference in page structures like we did in serial insertions:
SELECT [TableName] ,[FileID] ,[PageID] ,[CaptureTime] ,[m_pageId] ,[m_slotCnt] ,[m_freeCnt] ,[m_nextPage] ,[m_prevPage] ,[CombinedHash] ,[m_type] FROM [dbo].[PageTracking] order by 1,2,3,4 GO
We get below output again in tabular format after running above query(Scroll to complete right for better view, also it is more interpretable if you copy it to excel).
TableName | FileID | PageID | CaptureTime | m_pageId | m_slotCnt | m_freeCnt | m_nextPage | m_prevPage | CombinedHash | m_type |
test_page_splits_and_allocations | 1 | 1735636 | 2025-07-01 23:28:54 | (1:1735636) | 2 | 6 | (0:0) | (0:0) | 0x4478E108CE69A86DC99F88370E59540E592AEFEB4B44A4B41DFDECABFBCBA590F8F50754C1596F96E5536EB64DD2D9EA3EAAE37CAC66E9CAD42E3B13DBCF4261 | 10 |
test_page_splits_and_allocations | 1 | 1735636 | 2025-07-01 23:29:25 | (1:1735636) | 2 | 6 | (0:0) | (0:0) | 0x4478E108CE69A86DC99F88370E59540E592AEFEB4B44A4B41DFDECABFBCBA590F8F50754C1596F96E5536EB64DD2D9EA3EAAE37CAC66E9CAD42E3B13DBCF4261 | 10 |
test_page_splits_and_allocations | 1 | 2053048 | 2025-07-01 23:28:54 | (1:2053048) | 2 | 2070 | (1:2053050) | (0:0) | 0xE52B21FA7F1689BD1197B22F7CBC611472574F0FF7DA2FA2AF854A4CDED3F284981B2472D8946A01F058B9E9D11D6879085FC5886CD5E88246D6C637E73A5504 | 1 |
test_page_splits_and_allocations | 1 | 2053048 | 2025-07-01 23:29:25 | (1:2053048) | 2 | 2070 | (1:2053061) | (1:2053062) | 0x4E68F08DEFF2B1CAA00D2956E452736FF35145D09B87D769F6655522EE3C3756A90F133FFBACAD6129637330A3BF160F49C9009DCCEDE97FA9FD35684B46BFC5 | 1 |
test_page_splits_and_allocations | 1 | 2053049 | 2025-07-01 23:28:54 | (1:2053049) | 10 | 7966 | (0:0) | (0:0) | 0x71E9EB0470033F7278A333911CA3C3E92B99DCE9E7F3EC65C99D8A9C53DBAD41CDD38F503B88C047DEF1111E7B0F157A5849BEEF1D9EF75FFA3B2F0B2467799C | 2 |
test_page_splits_and_allocations | 1 | 2053049 | 2025-07-01 23:29:25 | (1:2053049) | 14 | 7914 | (0:0) | (0:0) | 0x66F3FBA8A0BE9A303A2840F819B13EAA940CBAB8EDE02FCBCFD0F4F01BE37850881DB52AB9B998DF13AC2C134BBDF5FFA07E10BC5460C42BA83AC3BF375395AE | 2 |
test_page_splits_and_allocations | 1 | 2053050 | 2025-07-01 23:28:54 | (1:2053050) | 2 | 2070 | (1:2053051) | (1:2053048) | 0xC588DE64F1266CCB99B2FDFEFDEAB1694645C71537D557423928EC2A78207BAC79D5F6E830F0EFBD943C5EA58842BF6F4D183EF448277B8B34F803E690089B01 | 1 |
test_page_splits_and_allocations | 1 | 2053050 | 2025-07-01 23:29:25 | (1:2053050) | 2 | 2070 | (1:2053051) | (1:2053060) | 0x9BAEF6D62DC9A5E80D3589DD35B165CB4A3EA7A58984067837D82A9C5B5A8856CCF11739E9FA3C31375E8CEDEDEB4E85DD6B5B0B750EABCE1F8D542A50692AC6 | 1 |
test_page_splits_and_allocations | 1 | 2053051 | 2025-07-01 23:28:54 | (1:2053051) | 2 | 2070 | (1:2053052) | (1:2053050) | 0x4304E3872DF1B2E015CFFF38C907790642B7BA7D0CFFAC40D1776A6400439BE2E8CC777A7343A5E84011EBEF6275717421113F0B1CC70C7A92B46A19BF2FF8E3 | 1 |
test_page_splits_and_allocations | 1 | 2053051 | 2025-07-01 23:29:25 | (1:2053051) | 2 | 2070 | (1:2053052) | (1:2053050) | 0x4304E3872DF1B2E015CFFF38C907790642B7BA7D0CFFAC40D1776A6400439BE2E8CC777A7343A5E84011EBEF6275717421113F0B1CC70C7A92B46A19BF2FF8E3 | 1 |
test_page_splits_and_allocations | 1 | 2053052 | 2025-07-01 23:28:54 | (1:2053052) | 2 | 2070 | (1:2053053) | (1:2053051) | 0xF863FECBEDB3EB691343E383B3815FFDCF1E0640D6188CEB2E1B66D2A377725A540802A2E2E455912A15286FC464E78B9A18C6E7770834206714DCD2CC951BB2 | 1 |
test_page_splits_and_allocations | 1 | 2053052 | 2025-07-01 23:29:25 | (1:2053052) | 2 | 2070 | (1:2053053) | (1:2053051) | 0xF863FECBEDB3EB691343E383B3815FFDCF1E0640D6188CEB2E1B66D2A377725A540802A2E2E455912A15286FC464E78B9A18C6E7770834206714DCD2CC951BB2 | 1 |
test_page_splits_and_allocations | 1 | 2053053 | 2025-07-01 23:28:54 | (1:2053053) | 2 | 2070 | (1:2053054) | (1:2053052) | 0x8716E97676E99366F3668C17DE89EF7E4A9298801D4E0A67C88B2A30B4387D7142D941DE7045A8A6750BAF9F4DF39E56282B1DF5DBEA0C4C70BEAE604A1D9597 | 1 |
test_page_splits_and_allocations | 1 | 2053053 | 2025-07-01 23:29:25 | (1:2053053) | 2 | 2070 | (1:2053054) | (1:2053052) | 0x8716E97676E99366F3668C17DE89EF7E4A9298801D4E0A67C88B2A30B4387D7142D941DE7045A8A6750BAF9F4DF39E56282B1DF5DBEA0C4C70BEAE604A1D9597 | 1 |
test_page_splits_and_allocations | 1 | 2053054 | 2025-07-01 23:28:54 | (1:2053054) | 2 | 2070 | (1:2053055) | (1:2053053) | 0x3CF7E5FAAC736BB6A19DE5B8627A6AD9ADA270C82F068A97DC0359D767D05C8E4272E575FC55CAF9BA0ABFC19D19307E29630D4E0B61690B10123AE6CBC73715 | 1 |
test_page_splits_and_allocations | 1 | 2053054 | 2025-07-01 23:29:25 | (1:2053054) | 2 | 2070 | (1:2053055) | (1:2053053) | 0x3CF7E5FAAC736BB6A19DE5B8627A6AD9ADA270C82F068A97DC0359D767D05C8E4272E575FC55CAF9BA0ABFC19D19307E29630D4E0B61690B10123AE6CBC73715 | 1 |
test_page_splits_and_allocations | 1 | 2053055 | 2025-07-01 23:28:54 | (1:2053055) | 2 | 2070 | (1:2053056) | (1:2053054) | 0xA0034546C423B90135F0E4C41E3DF3C8CF3BCB5AA03336BAF5D47BE0B59335B035ACB87AF70B611025340A967A2DA4F6CB546BDA3060823083C5B10C768F9CBD | 1 |
test_page_splits_and_allocations | 1 | 2053055 | 2025-07-01 23:29:25 | (1:2053055) | 2 | 2070 | (1:2053056) | (1:2053054) | 0xA0034546C423B90135F0E4C41E3DF3C8CF3BCB5AA03336BAF5D47BE0B59335B035ACB87AF70B611025340A967A2DA4F6CB546BDA3060823083C5B10C768F9CBD | 1 |
test_page_splits_and_allocations | 1 | 2053056 | 2025-07-01 23:28:54 | (1:2053056) | 2 | 2070 | (1:2053057) | (1:2053055) | 0xD95ED019FF4F87F1ECB30DF5D32835088926BE120D9130FBCC300F4FA984481987CD2198EFF776FE1D10DBA86864562BB02A1413B367B93AAE67EF2FBFB8A5F3 | 1 |
test_page_splits_and_allocations | 1 | 2053056 | 2025-07-01 23:29:26 | (1:2053056) | 2 | 2070 | (1:2053057) | (1:2053055) | 0xD95ED019FF4F87F1ECB30DF5D32835088926BE120D9130FBCC300F4FA984481987CD2198EFF776FE1D10DBA86864562BB02A1413B367B93AAE67EF2FBFB8A5F3 | 1 |
test_page_splits_and_allocations | 1 | 2053057 | 2025-07-01 23:28:54 | (1:2053057) | 2 | 2070 | (1:2053058) | (1:2053056) | 0x96148080700678DDCC58880B6E46F2A7D455235E43AB50B3C2096B3169DB805D1A8765A777C2DF25EC1388A3CC37A81B19618C100DC156F5C5E2D2CC6754CE3E | 1 |
test_page_splits_and_allocations | 1 | 2053057 | 2025-07-01 23:29:26 | (1:2053057) | 2 | 2070 | (1:2053058) | (1:2053056) | 0x96148080700678DDCC58880B6E46F2A7D455235E43AB50B3C2096B3169DB805D1A8765A777C2DF25EC1388A3CC37A81B19618C100DC156F5C5E2D2CC6754CE3E | 1 |
test_page_splits_and_allocations | 1 | 2053058 | 2025-07-01 23:28:54 | (1:2053058) | 2 | 2070 | (0:0) | (1:2053057) | 0x99AC6BD915CD504662AFA24069DA832032A39308E8101DAF8C05B889B0FE2048DB75BAA95F70B7C46CA26B4B954253D4D8B367198E71AE1637233B1153F83E9B | 1 |
test_page_splits_and_allocations | 1 | 2053058 | 2025-07-01 23:29:26 | (1:2053058) | 2 | 2070 | (0:0) | (1:2053057) | 0x99AC6BD915CD504662AFA24069DA832032A39308E8101DAF8C05B889B0FE2048DB75BAA95F70B7C46CA26B4B954253D4D8B367198E71AE1637233B1153F83E9B | 1 |
test_page_splits_and_allocations | 1 | 2053059 | 2025-07-01 23:28:54 | (1:2053059) | 1 | 8063 | (0:0) | (1:2044936) | 0xC9F6B04BFA1FA70D9E43C505BC2DB284E2C6FB8EC5714C10D3E7C5242CC249BDDF67685B7D599083C1867B90777F2F01073DAA8653EAAB60E7C5533E99A00D87 | 1 |
test_page_splits_and_allocations | 1 | 2053059 | 2025-07-01 23:29:26 | (1:2053059) | 2 | 2070 | (1:2053062) | (0:0) | 0xA7695E9550F33F2587AB657273A9B5B425AF0CBEF06B8C21E9ED3BBC03EB069391D211531D02AB107BFDF9BAE1D21E07DBC5DFC779C7DDBAFFD3356D398EA783 | 1 |
test_page_splits_and_allocations | 1 | 2053060 | 2025-07-01 23:28:54 | (1:2053060) | 1 | 8063 | (0:0) | (1:2044936) | 0x09D5F90D70A9A5752D1A07D37C2CBC874059B82B90E06883D1D3AB3B5BE7E1F185973A7813DF87B1B92B637970A0223052794CA11C0AAF0C59C6BFD7B4E78F31 | 1 |
test_page_splits_and_allocations | 1 | 2053060 | 2025-07-01 23:29:26 | (1:2053060) | 1 | 5083 | (1:2053050) | (1:2053061) | 0x2BA178F1AAAF41BF242A64D63389C89E16628D5CD7F539AB2B6FF83A20A4664D44E2F0705E8E1E4DB359049851FA090D9C7575B31848D65F31DD284653A85829 | 1 |
test_page_splits_and_allocations | 1 | 2053061 | 2025-07-01 23:28:54 | (1:2053061) | 1 | 8063 | (0:0) | (1:2044936) | 0x511FDEAE4ECA8C73E3350D962780E7E6836F4252C428B0AA8F7AE7B88574AA9C647F95F579726DCDEA6DB2852090B19AE6D025840AEC9E04774E837E619BA19A | 1 |
test_page_splits_and_allocations | 1 | 2053061 | 2025-07-01 23:29:26 | (1:2053061) | 1 | 5083 | (1:2053060) | (1:2053048) | 0x6D980DA9023F54FD2E02C28C6AC8C388A40D09EB3257137C19BC5C3532CC7B457F3EDE41FAB6857496DD0FC7D8A0550531BFD360D8AB1D34FAE9839BA9B7F157 | 1 |
test_page_splits_and_allocations | 1 | 2053062 | 2025-07-01 23:28:54 | (1:2053062) | 1 | 8063 | (0:0) | (1:2044936) | 0x1742C598E71B4420EAE292BA102207339E2FB1C3E2C623C38C28CD357E3DD8697A4F5D478A8701049228A36FF1C039AF791FA39998CB8F4CBBA4C0913A01F5B0 | 1 |
test_page_splits_and_allocations | 1 | 2053062 | 2025-07-01 23:29:26 | (1:2053062) | 1 | 5083 | (1:2053048) | (1:2053059) | 0x9E839CF862F402764DFB7D7CE96E13CBCA6D6CA10CEC1162F1502D5080EF8B838A9BA1445E14E2FF551486BFE36ED16CDE7845AA7A76E5ED28CB2214193D6492 | 1 |
test_page_splits_and_allocations | 1 | 2053063 | 2025-07-01 23:28:54 | (1:2053063) | 1 | 8063 | (0:0) | (1:2044936) | 0xE1A632F057B4BF284F4BC9B83659C184F3948561F8E30A373EE222209C45AD80DD1BBC0C7B3C2574EC3886A02AA91E046C2690F6FC7E6D30C5D9E1FFADC3D736 | 1 |
test_page_splits_and_allocations | 1 | 2053063 | 2025-07-01 23:29:26 | (1:2053063) | 1 | 8063 | (0:0) | (1:2044936) | 0xE1A632F057B4BF284F4BC9B83659C184F3948561F8E30A373EE222209C45AD80DD1BBC0C7B3C2574EC3886A02AA91E046C2690F6FC7E6D30C5D9E1FFADC3D736 | 1
|
Explaination(Table output After Mid Table Insert)
Now lets compare structure of page before and after inserts. The pages which are changed are marked in bold. Those pages are 2053048, 2053049, , 2053050,2053059,2053060,2053061, 2053062.(Scroll to right for better view, also export to excel for better interpretibility)
Page 2053048
m_freeCnt remained 2070, but m_nextPage changed from (1:2053050) to (1:2053061) and m_prevPage updated from (0:0) to (1:2053062). This indicates a split event occurred – it was previously linked directly to page 2053050 but now links to a new page (2053061) while also getting a new previous page (2053062). Such page chain updates without reduction in free space strongly suggest row movement due to split redistribution rather than direct insert filling space.
Page 2053049
m_slotCnt increased from 10 to 14 and m_freeCnt decreased slightly from 7966 to 7914, indicating new rows were inserted here. However, no m_nextPage or m_prevPage changes were observed, so no split or row movement occurred within this page – it was simply updated with additional index entries.
Page 2053050
m_freeCnt remained 2070, but m_prevPage changed from (1:2053048) to (1:2053060) while m_nextPage stayed at (1:2053051). This implies a split event upstream caused re-linking – the page itself was not split, but row movement from adjacent split required the chain to update its previous page pointer.
Page 2053059
Here m_slotCnt increased from 1 to 2 and m_freeCnt dropped from 8063 to 2070, with m_nextPage changing from (0:0) to (1:2053062). This confirms new rows were inserted consuming space, and a new next page was linked, suggesting a split occurred where this page remained but a new sibling page (2053062) was created to accommodate overflow rows – row movement due to split is evident.
Page 2053060
m_freeCnt dropped significantly from 8063 to 5083, indicating new rows were inserted. Its m_nextPage changed from (0:0) to (1:2053050), and m_prevPage updated from (1:2044936) to (1:2053061), showing it was inserted between pages during a split. Thus, this page likely represents a split result page where row movement occurred to balance tree structure.
Page 2053061
This page’s m_freeCnt also reduced from 8063 to 5083, and m_prevPage changed from (1:2044936) to (1:2053048) while m_nextPage updated to (1:2053060). The combination of decreased free space and changed links indicates it absorbed moved rows from a split, confirming split event with row redistribution.
Page 2053062
Here, m_freeCnt dropped from 8063 to 5083, m_nextPage changed from (0:0) to (1:2053048), and m_prevPage updated to (1:2053059). The reduction in free space along with linking back to 2053048 confirms it was newly created due to a split, receiving moved rows from the original page.
Conclusion
This forensic analysis clearly demonstrates why mid-table inserts are significantly costlier than serial inserts. In our test, we observed multiple pages undergoing splits where row movement occurred to balance data across old and new pages. Pages such as 2053048, 2053050, 2053059, and others showed evidence of chain re-linking, creation of new sibling pages, and redistribution of rows. Unlike serial inserts, which mostly add rows to the end of the index without disturbing existing pages, out-of-order or mid-table inserts force SQL Server to reorganize data structures to maintain logical order.
Such page splits are resource-intensive operations because they involve copying approximately half the rows to a new page, updating page pointers, and maintaining index consistency, often resulting in significant I/O and CPU overhead. If your workload frequently inserts data that is not strictly sequential – for example, inserting older timestamps or updating clustered key values – you will almost certainly encounter these performance penalties.
To mitigate this, consider optimising your index design and data load patterns. Using a lower fill factor can leave breathing space within pages to accommodate future inserts without immediate splits, though it increases overall storage requirements. Alternatively, adopting table partitioning based on time or ranges can isolate inserts to specific partitions, avoiding broad impact across the entire table. Understanding these low-level behaviours is crucial for DBAs and architects aiming to build scalable, insert-heavy systems without encountering sudden slowdowns due to silent page split storms during peak workloads.
Caution
This script uses DBCC PAGE, an undocumented and unsupported command intended for internal use and troubleshooting by advanced DBAs or Microsoft support engineers. Running it on large tables or in frequent automated loops may cause performance overhead due to intensive metadata reads and console output generation. Always test in non-production environments before deploying in live systems. Use with appropriate permissions and ensure you understand the risks of reading low-level page structures. Results and internal structures may vary between SQL Server versions, so validate thoroughly on your target environment before relying on it for operational decisions.