SQLServerCentral Article

Advanced SQL Server Page Forensics: Detecting Page Splits and Allocations with DBCC PAGE

,

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.

USE simple_talk;
GO

IF OBJECT_ID('dbo.test_page_splits_and_allocations', 'U') IS NOT NULL
    DROP TABLE dbo.test_page_splits_and_allocations;
GO

CREATE TABLE dbo.test_page_splits_and_allocations
(
    Id INT PRIMARY KEY,
    Data1 CHAR(1000),
    Data2 CHAR(1000),
    Data3 CHAR(1000)
);
GO

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.

DECLARE @i INT = 10;

WHILE @i <= 200
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 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).

 

TableNameFileIDPageIDCaptureTimem_pageIdm_slotCntm_freeCntm_nextPagem_prevPageCombinedHashm_type
test_page_splits_and_allocations117356362025-07-01 22:49:45(1:1735636)26(0:0)(0:0)0x4478E108CE69A86DC99F88370E59540E592AEFEB4B44A4B41DFDECABFBCBA590F8F50754C1596F96E5536EB64DD2D9EA3EAAE37CAC66E9CAD42E3B13DBCF426110
test_page_splits_and_allocations117356362025-07-01 22:50:43(1:1735636)26(0:0)(0:0)0x4478E108CE69A86DC99F88370E59540E592AEFEB4B44A4B41DFDECABFBCBA590F8F50754C1596F96E5536EB64DD2D9EA3EAAE37CAC66E9CAD42E3B13DBCF426110
test_page_splits_and_allocations121340242025-07-01 22:49:45(1:2134024)22070(1:2134028)(0:0)0x046933607CC80DE6A56D8355B37D63A7BE45EFEE3B2706C62E52A641208B0C7EB53295A3B12609BE37E00F9FBE00147716E912A616C51376F9F88CC54C6077651
test_page_splits_and_allocations121340242025-07-01 22:50:43(1:2134024)22070(1:2134028)(0:0)0x046933607CC80DE6A56D8355B37D63A7BE45EFEE3B2706C62E52A641208B0C7EB53295A3B12609BE37E00F9FBE00147716E912A616C51376F9F88CC54C6077651
test_page_splits_and_allocations121340252025-07-01 22:49:45(1:2134025)48044(0:0)(0:0)0x4DC7CCCE858D0A3373C5AC4EBF970FFB4DF838FC959D2AB13DE82596C41A40658A7421BB4E9830CD2EC6EF9D859A1909EF73B52F481E6CA6B65045FD0B21AAEB2
test_page_splits_and_allocations121340252025-07-01 22:50:44(1:2134025)147914(0:0)(0:0)0xC51A3F99D9FBB870B658DB99B860B542487DFD35FDD5ACD57C0F9A921D8CCF3F40B3D2BD85C381DD91B46F9753C8E224E777205480585E87DD27E793A14B3B0F2
test_page_splits_and_allocations121340262025-07-01 22:49:45(1:2134026)15083(0:0)(1:2134027)0xF5A1A47AFED6877544D91F7F8A97BC67563832F8591184B994E7178A89F8F6667BF7FA19463E74247485B3DD520B9419E318BC51134409A86C545ED8905630531
test_page_splits_and_allocations121340262025-07-01 22:50:44(1:2134026)22070(1:2134029)(1:2134027)0x89659601FF477DBAD14F89E92BA991FA60C71CCB7CF0E988E76EA6EEFC212770D7E73FCFFE758E164919697CBC685ABADC17A8FE7BCE8DEFBA25D0BBAB3463D11
test_page_splits_and_allocations121340272025-07-01 22:49:45(1:2134027)15083(1:2134026)(1:2134028)0x57D264DBF449342FC5073C99B129F69DC96DE0ADEBCC5F24A08348BD4BAB5271598E4C741826D41ACB79317FA7309ABA822D5BF69402D281323ED6322D3BA7841
test_page_splits_and_allocations121340272025-07-01 22:50:44(1:2134027)15083(1:2134026)(1:2134028)0x57D264DBF449342FC5073C99B129F69DC96DE0ADEBCC5F24A08348BD4BAB5271598E4C741826D41ACB79317FA7309ABA822D5BF69402D281323ED6322D3BA7841
test_page_splits_and_allocations121340282025-07-01 22:49:45(1:2134028)15083(1:2134027)(1:2134024)0x4FE7A78A2B77F6AF19712599DDC10DEAC65E7101BEA44B0C3D8E62CEFB3E92EC85C6A908C02D37F1D8CC4E4BC840B76BB013C362C71E934732544517EB756BCD1
test_page_splits_and_allocations121340282025-07-01 22:50:44(1:2134028)15083(1:2134027)(1:2134024)0x4FE7A78A2B77F6AF19712599DDC10DEAC65E7101BEA44B0C3D8E62CEFB3E92EC85C6A908C02D37F1D8CC4E4BC840B76BB013C362C71E934732544517EB756BCD1
test_page_splits_and_allocations121340292025-07-01 22:49:45(1:2134029)18063(0:0)(1:2044936)0x9E097F98EFE72471D1162E984471487C1358B248FE05FBFFDB5AB157D0675AE52014057F94A27BBDF79599439F03253D42F607DF8C34A24AAC22AB0CF746FD521
test_page_splits_and_allocations121340292025-07-01 22:50:44(1:2134029)22070(1:2134030)(1:2134026)0x81DD7E21545AB637836BB9A763FB96E9E154E7358D898FD11FA1EEB454A2FF3D72B8D75C044E914034FA1CEBB52BADD44BDB8770B8AF88A9180BAFA8EE6263271
test_page_splits_and_allocations121340302025-07-01 22:49:45(1:2134030)18063(0:0)(1:2044936)0x982DD483E634B952F0BE3413037C8370DFB2D0634F14149E961D74E374258F8B0DF4D9BEC9F46C4144D5A3BB7C255CB729FEA42464C31DDF1D953B4A6C2561361
test_page_splits_and_allocations121340302025-07-01 22:50:44(1:2134030)22070(1:2134031)(1:2134029)0xC1F3E69BB77FA1DFA4AD8391F254E2BE472F3D8AD4D7F8BAC560CE3EE45012E62F9FBF24C4849FF5D9F404E72843B3AB6B3966775C97392ACF35E99BD5BF4B161
test_page_splits_and_allocations121340312025-07-01 22:49:45(1:2134031)18063(0:0)(1:2044936)0xEFA3B2611D43702D2667D13037FA115471E58DBEE2D9824A9BA1853396E5DFA25EEE21ADB32707AEBE83995F077F84C7B5FDE04619670960B42EF86513030AA11
test_page_splits_and_allocations121340312025-07-01 22:50:44(1:2134031)22070(1:2142128)(1:2134030)0xA786653485CD464E7F32E6B7BF15574CF4F13115E688786FD76B995C22BE84CDCF359FE146EC52ABCE29374379DE9ADF522B60F938EAEBC99A5A3749D22433311
test_page_splits_and_allocations121421282025-07-01 22:50:44(1:2142128)22070(1:2142129)(1:2134031)0xF40DDD37116F66B32C466254A5BBDBD219B6CD30C40F2E515BE1323CBB4FADD106D5124AB5D2192B6C2DF13D1D8F27D0918B88580F53AE5E3924444B1632E8491
test_page_splits_and_allocations121421292025-07-01 22:50:44(1:2142129)22070(1:2142130)(1:2142128)0x4D0C4F741374EE7EBE505FD48AD971A04BEE874C81221400C5C036AC7D21ACC588B331792C12B2ACDA09D91E6316FF2D626F35889A5C0CF4A6474E51966A822D1
test_page_splits_and_allocations121421302025-07-01 22:50:44(1:2142130)22070(1:2142131)(1:2142129)0x8D95E8FBD51EAD906EBAC5E179D524366E89A0DE20F71A1E7D2CCE4F1878B39A050ED7C5046EE15E7D507C472E7C42B557208F65A9EB57638550C6D2643FC4DD1
test_page_splits_and_allocations121421312025-07-01 22:50:44(1:2142131)22070(1:2142132)(1:2142130)0xED962EC559C82FD8992B218B81FE5E69BA39B9CEEF8A8F7742F1D81F2EF897A313CF8722338E7FA02243DF694E87A93BA5954C99B7FAA38E946C6BA054127F1D1
test_page_splits_and_allocations121421322025-07-01 22:50:44(1:2142132)22070(1:2142133)(1:2142131)0x38BAAD39C90E83B57C4DE2A44121D140730CA38E84A5793839EC2EED5AA6DD6EE06D022007B563D1DAAF513F6AC4178920230D7234E395B1CE1A71895D36BEA61
test_page_splits_and_allocations121421332025-07-01 22:50:44(1:2142133)22070(1:2142134)(1:2142132)0x1C146BA8D4B014DCEC47548F9E75AF9DCCB5A3E90B1442E0074DF97F112E57FE505CAC9708E675023E79DE125C3675012BFC9618D35E72A66D952F7CDC659CF71
test_page_splits_and_allocations121421342025-07-01 22:50:44(1:2142134)15083(0:0)(1:2142133)0xB6541C90F2494C742065B1E2F57DDE61B2669804A2EAFC040B1A9373B5857253FD15A908724745A9416BA26A0C978ED958AB385D8AEE4D96666B3776ED5F745E1
test_page_splits_and_allocations121421352025-07-01 22:50:44(1:2142135)18063(0:0)(1:2044936)0x11706C5755826A8B63A1DA59A8D935D44CE00F60933997F970B3BED2C6C794B2B3A13CD580B0BE0B82BB0041FDE1E383C2A3AFF510F111DF02CB38E0652E96D31

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.

INSERT INTO dbo.test_page_splits_and_allocations (Id, Data1, Data2, Data3)
VALUES 
(5, REPLICATE('X', 1000), REPLICATE('Y', 1000), REPLICATE('Z', 1000)),
(11, REPLICATE('X', 1000), REPLICATE('Y', 1000), REPLICATE('Z', 1000)),
(15, REPLICATE('X', 1000), REPLICATE('Y', 1000), REPLICATE('Z', 1000)),
(7, REPLICATE('X', 1000), REPLICATE('Y', 1000), REPLICATE('Z', 1000)),
(3, REPLICATE('X', 1000), REPLICATE('Y', 1000), REPLICATE('Z', 1000));
GO

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).

 

 

TableNameFileIDPageIDCaptureTimem_pageIdm_slotCntm_freeCntm_nextPagem_prevPageCombinedHashm_type
test_page_splits_and_allocations117356362025-07-01 23:28:54(1:1735636)26(0:0)(0:0)0x4478E108CE69A86DC99F88370E59540E592AEFEB4B44A4B41DFDECABFBCBA590F8F50754C1596F96E5536EB64DD2D9EA3EAAE37CAC66E9CAD42E3B13DBCF426110
test_page_splits_and_allocations117356362025-07-01 23:29:25(1:1735636)26(0:0)(0:0)0x4478E108CE69A86DC99F88370E59540E592AEFEB4B44A4B41DFDECABFBCBA590F8F50754C1596F96E5536EB64DD2D9EA3EAAE37CAC66E9CAD42E3B13DBCF426110
test_page_splits_and_allocations120530482025-07-01 23:28:54(1:2053048)22070(1:2053050)(0:0)0xE52B21FA7F1689BD1197B22F7CBC611472574F0FF7DA2FA2AF854A4CDED3F284981B2472D8946A01F058B9E9D11D6879085FC5886CD5E88246D6C637E73A55041
test_page_splits_and_allocations120530482025-07-01 23:29:25(1:2053048)22070(1:2053061)(1:2053062)0x4E68F08DEFF2B1CAA00D2956E452736FF35145D09B87D769F6655522EE3C3756A90F133FFBACAD6129637330A3BF160F49C9009DCCEDE97FA9FD35684B46BFC51
test_page_splits_and_allocations120530492025-07-01 23:28:54(1:2053049)107966(0:0)(0:0)0x71E9EB0470033F7278A333911CA3C3E92B99DCE9E7F3EC65C99D8A9C53DBAD41CDD38F503B88C047DEF1111E7B0F157A5849BEEF1D9EF75FFA3B2F0B2467799C2
test_page_splits_and_allocations120530492025-07-01 23:29:25(1:2053049)147914(0:0)(0:0)0x66F3FBA8A0BE9A303A2840F819B13EAA940CBAB8EDE02FCBCFD0F4F01BE37850881DB52AB9B998DF13AC2C134BBDF5FFA07E10BC5460C42BA83AC3BF375395AE2
test_page_splits_and_allocations120530502025-07-01 23:28:54(1:2053050)22070(1:2053051)(1:2053048)0xC588DE64F1266CCB99B2FDFEFDEAB1694645C71537D557423928EC2A78207BAC79D5F6E830F0EFBD943C5EA58842BF6F4D183EF448277B8B34F803E690089B011
test_page_splits_and_allocations120530502025-07-01 23:29:25(1:2053050)22070(1:2053051)(1:2053060)0x9BAEF6D62DC9A5E80D3589DD35B165CB4A3EA7A58984067837D82A9C5B5A8856CCF11739E9FA3C31375E8CEDEDEB4E85DD6B5B0B750EABCE1F8D542A50692AC61
test_page_splits_and_allocations120530512025-07-01 23:28:54(1:2053051)22070(1:2053052)(1:2053050)0x4304E3872DF1B2E015CFFF38C907790642B7BA7D0CFFAC40D1776A6400439BE2E8CC777A7343A5E84011EBEF6275717421113F0B1CC70C7A92B46A19BF2FF8E31
test_page_splits_and_allocations120530512025-07-01 23:29:25(1:2053051)22070(1:2053052)(1:2053050)0x4304E3872DF1B2E015CFFF38C907790642B7BA7D0CFFAC40D1776A6400439BE2E8CC777A7343A5E84011EBEF6275717421113F0B1CC70C7A92B46A19BF2FF8E31
test_page_splits_and_allocations120530522025-07-01 23:28:54(1:2053052)22070(1:2053053)(1:2053051)0xF863FECBEDB3EB691343E383B3815FFDCF1E0640D6188CEB2E1B66D2A377725A540802A2E2E455912A15286FC464E78B9A18C6E7770834206714DCD2CC951BB21
test_page_splits_and_allocations120530522025-07-01 23:29:25(1:2053052)22070(1:2053053)(1:2053051)0xF863FECBEDB3EB691343E383B3815FFDCF1E0640D6188CEB2E1B66D2A377725A540802A2E2E455912A15286FC464E78B9A18C6E7770834206714DCD2CC951BB21
test_page_splits_and_allocations120530532025-07-01 23:28:54(1:2053053)22070(1:2053054)(1:2053052)0x8716E97676E99366F3668C17DE89EF7E4A9298801D4E0A67C88B2A30B4387D7142D941DE7045A8A6750BAF9F4DF39E56282B1DF5DBEA0C4C70BEAE604A1D95971
test_page_splits_and_allocations120530532025-07-01 23:29:25(1:2053053)22070(1:2053054)(1:2053052)0x8716E97676E99366F3668C17DE89EF7E4A9298801D4E0A67C88B2A30B4387D7142D941DE7045A8A6750BAF9F4DF39E56282B1DF5DBEA0C4C70BEAE604A1D95971
test_page_splits_and_allocations120530542025-07-01 23:28:54(1:2053054)22070(1:2053055)(1:2053053)0x3CF7E5FAAC736BB6A19DE5B8627A6AD9ADA270C82F068A97DC0359D767D05C8E4272E575FC55CAF9BA0ABFC19D19307E29630D4E0B61690B10123AE6CBC737151
test_page_splits_and_allocations120530542025-07-01 23:29:25(1:2053054)22070(1:2053055)(1:2053053)0x3CF7E5FAAC736BB6A19DE5B8627A6AD9ADA270C82F068A97DC0359D767D05C8E4272E575FC55CAF9BA0ABFC19D19307E29630D4E0B61690B10123AE6CBC737151
test_page_splits_and_allocations120530552025-07-01 23:28:54(1:2053055)22070(1:2053056)(1:2053054)0xA0034546C423B90135F0E4C41E3DF3C8CF3BCB5AA03336BAF5D47BE0B59335B035ACB87AF70B611025340A967A2DA4F6CB546BDA3060823083C5B10C768F9CBD1
test_page_splits_and_allocations120530552025-07-01 23:29:25(1:2053055)22070(1:2053056)(1:2053054)0xA0034546C423B90135F0E4C41E3DF3C8CF3BCB5AA03336BAF5D47BE0B59335B035ACB87AF70B611025340A967A2DA4F6CB546BDA3060823083C5B10C768F9CBD1
test_page_splits_and_allocations120530562025-07-01 23:28:54(1:2053056)22070(1:2053057)(1:2053055)0xD95ED019FF4F87F1ECB30DF5D32835088926BE120D9130FBCC300F4FA984481987CD2198EFF776FE1D10DBA86864562BB02A1413B367B93AAE67EF2FBFB8A5F31
test_page_splits_and_allocations120530562025-07-01 23:29:26(1:2053056)22070(1:2053057)(1:2053055)0xD95ED019FF4F87F1ECB30DF5D32835088926BE120D9130FBCC300F4FA984481987CD2198EFF776FE1D10DBA86864562BB02A1413B367B93AAE67EF2FBFB8A5F31
test_page_splits_and_allocations120530572025-07-01 23:28:54(1:2053057)22070(1:2053058)(1:2053056)0x96148080700678DDCC58880B6E46F2A7D455235E43AB50B3C2096B3169DB805D1A8765A777C2DF25EC1388A3CC37A81B19618C100DC156F5C5E2D2CC6754CE3E1
test_page_splits_and_allocations120530572025-07-01 23:29:26(1:2053057)22070(1:2053058)(1:2053056)0x96148080700678DDCC58880B6E46F2A7D455235E43AB50B3C2096B3169DB805D1A8765A777C2DF25EC1388A3CC37A81B19618C100DC156F5C5E2D2CC6754CE3E1
test_page_splits_and_allocations120530582025-07-01 23:28:54(1:2053058)22070(0:0)(1:2053057)0x99AC6BD915CD504662AFA24069DA832032A39308E8101DAF8C05B889B0FE2048DB75BAA95F70B7C46CA26B4B954253D4D8B367198E71AE1637233B1153F83E9B1
test_page_splits_and_allocations120530582025-07-01 23:29:26(1:2053058)22070(0:0)(1:2053057)0x99AC6BD915CD504662AFA24069DA832032A39308E8101DAF8C05B889B0FE2048DB75BAA95F70B7C46CA26B4B954253D4D8B367198E71AE1637233B1153F83E9B1
test_page_splits_and_allocations120530592025-07-01 23:28:54(1:2053059)18063(0:0)(1:2044936)0xC9F6B04BFA1FA70D9E43C505BC2DB284E2C6FB8EC5714C10D3E7C5242CC249BDDF67685B7D599083C1867B90777F2F01073DAA8653EAAB60E7C5533E99A00D871
test_page_splits_and_allocations120530592025-07-01 23:29:26(1:2053059)22070(1:2053062)(0:0)0xA7695E9550F33F2587AB657273A9B5B425AF0CBEF06B8C21E9ED3BBC03EB069391D211531D02AB107BFDF9BAE1D21E07DBC5DFC779C7DDBAFFD3356D398EA7831
test_page_splits_and_allocations120530602025-07-01 23:28:54(1:2053060)18063(0:0)(1:2044936)0x09D5F90D70A9A5752D1A07D37C2CBC874059B82B90E06883D1D3AB3B5BE7E1F185973A7813DF87B1B92B637970A0223052794CA11C0AAF0C59C6BFD7B4E78F311
test_page_splits_and_allocations120530602025-07-01 23:29:26(1:2053060)15083(1:2053050)(1:2053061)0x2BA178F1AAAF41BF242A64D63389C89E16628D5CD7F539AB2B6FF83A20A4664D44E2F0705E8E1E4DB359049851FA090D9C7575B31848D65F31DD284653A858291
test_page_splits_and_allocations120530612025-07-01 23:28:54(1:2053061)18063(0:0)(1:2044936)0x511FDEAE4ECA8C73E3350D962780E7E6836F4252C428B0AA8F7AE7B88574AA9C647F95F579726DCDEA6DB2852090B19AE6D025840AEC9E04774E837E619BA19A1
test_page_splits_and_allocations120530612025-07-01 23:29:26(1:2053061)15083(1:2053060)(1:2053048)0x6D980DA9023F54FD2E02C28C6AC8C388A40D09EB3257137C19BC5C3532CC7B457F3EDE41FAB6857496DD0FC7D8A0550531BFD360D8AB1D34FAE9839BA9B7F1571
test_page_splits_and_allocations120530622025-07-01 23:28:54(1:2053062)18063(0:0)(1:2044936)0x1742C598E71B4420EAE292BA102207339E2FB1C3E2C623C38C28CD357E3DD8697A4F5D478A8701049228A36FF1C039AF791FA39998CB8F4CBBA4C0913A01F5B01
test_page_splits_and_allocations120530622025-07-01 23:29:26(1:2053062)15083(1:2053048)(1:2053059)0x9E839CF862F402764DFB7D7CE96E13CBCA6D6CA10CEC1162F1502D5080EF8B838A9BA1445E14E2FF551486BFE36ED16CDE7845AA7A76E5ED28CB2214193D64921
test_page_splits_and_allocations120530632025-07-01 23:28:54(1:2053063)18063(0:0)(1:2044936)0xE1A632F057B4BF284F4BC9B83659C184F3948561F8E30A373EE222209C45AD80DD1BBC0C7B3C2574EC3886A02AA91E046C2690F6FC7E6D30C5D9E1FFADC3D7361
test_page_splits_and_allocations120530632025-07-01 23:29:26(1:2053063)18063(0:0)(1:2044936)0xE1A632F057B4BF284F4BC9B83659C184F3948561F8E30A373EE222209C45AD80DD1BBC0C7B3C2574EC3886A02AA91E046C2690F6FC7E6D30C5D9E1FFADC3D7361

 

 

 

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.

Rate

5 (1)

You rated this post out of 5. Change rating

Share

Share

Rate

5 (1)

You rated this post out of 5. Change rating