SQLServerCentral Article

Table Partitioning –XE Diagnostic for Data Movements

,

If the partitioning MERGE command is executed for the wrong range after a SWITCH OUT of historic data, data movement between file groups may be necessary before or during the next index rebuild. The SQLCMD lab setup script at the end of this article:

  • Creates a test database and 2 partitioned tables, one table is range right, the other range left. Each contains 6 partitions, the first and last partitions are empty. The 4 middle partitions in each table contain data in the ranges between 1 to 999, 1,000 to 1,999, 2,000 to 2,999, 3,000 to 3,999.
  • Inserts 4,000 rows of test data
  • Data in the lowest populated partition range (1 – 999) is switched out, partition 2 in each table
  • The merge command is executed but at the wrong boundary for the RANGE RIGHT table (1001) causing a move of data between file groups.
  • Stop the script before running the MERGE final INDEX REBUILD’s, download the SQL Server Debug symbols and create an extended events session to capture the call stack.

Immediately after the MERGE / before the final INDEX REBUILD, the system ‘Data Space’ views for the RANGE RIGHT table show where the data will be (FG2), not where it actually still is (FG3). The data seems to have moved between file groups instantly but I think the operation has been deferred until the index rebuild.

The lower file group (FG2) is retained by the MERGE command when the partition scheme is RANGE RIGHT. The data previously resided in File Group 3 so a move is required before or during the next index rebuild.

Extended Events and the Call Stack

An extended event session was started before the MERGE and final index rebuilds (RANGE RIGHT & RANGE LEFT) and the call stack analysed for various wait types including WRITE_COMPLETION and IO_COMPLETION. There were no noticeable, consistent bucketized call stack differences for any of the wait types I tried. There were differences during ‘file-written’ events, both tables included the call stack below although it was invoked more for the RANGE RIGHT table with  it’s associated data movement between file groups

 XeSqlPkg::file_written::Publish+1ba [ @ 0+0x0 FireWriteEvent+16c [ @ 0+0x0 

FCB::AsyncWrite+1bc [ @ 0+0x0 
SQLServerLogMgr::FlushLC+7b4 [ @ 0+0x0 
SQLServerLogMgr::AppendLogRequest+1a2 [ @ 0+0x0 
SQLServerLogMgr::ReserveAndAppend+74 [ @ 0+0x0 
XdesRMReadWrite::GenerateLogRec+5ac [ @ 0+0x0 
PageRef::FormatFull+1a3 [ @ 0+0x0 
CBulkAllocator::FinishCurrentBuf+97 [ @ 0+0x0 
CBulkAllocator::AllocateLinkedAndFormattedLeafPage+f2 [ @ 0+0x0 
CIndBuild::AllocateNextIndexPage+15 [ @ 0+0x0 
CIndBuild::InsertRow+bf2 [ @ 0+0x0 
RowsetCreateIndex::InsertIndexRow+39e8 [ @ 0+0x0 
CValRowCrtIdx::SetDataX+1d6 [ @ 0+0x0 0x000007FE070F6FE7 
CQScanIndexNew::NonOptimizedGetAndInsertRows+f3 [ @ 0+0x0 
CQScanIndexNew::GetRow+31 [ @ 0+0x0 
CQScanNLJoinTrivialNew::GetRow+126 [ @ 0+0x0 
CQueryScan::GetRow+81 [ @ 0+0x0 
CXStmtQuery::ErsqExecuteQuery+36a [ @ 0+0x0 
CXStmtDML::XretDMLExecute+2f3 [ @ 0+0x0 
CXStmtDML::XretExecute+ad [ @ 0+0x0 
CMsqlExecContext::ExecuteStmts<1,1>+400 [ @ 0+0x0 
CMsqlExecContext::FExecute+a33 [ @ 0+0x0

The call stack below only appeared during INDEX REBUILD of the RANGE RIGHT table.

XeSqlPkg::file_written::Publish+1ba [ @ 0+0x0 FireWriteEvent+16c [ @ 0+0x0 

FCB::AsyncWrite+1bc [ @ 0+0x0 

SQLServerLogMgr::UpdateFileHdr+29b [ @ 0+0x0 

SQLServerLogMgr::FlushLC+97f [ @ 0+0x0 

SQLServerLogMgr::AppendLogRequest+1a2 [ @ 0+0x0 

SQLServerLogMgr::ReserveAndAppend+74 [ @ 0+0x0 

PageRef::ModifyBitsNonTransactional+3e4 [ @ 0+0x0 ChangeGhostPageState+116 [ @ 0+0x0 ChangeGhostPageState+129 [ @ 0+0x0 

PageRef::MarkPfsGhostState+24 [ @ 0+0x0 

IndexPageRef::MarkGhost+135e [ @ 0+0x0 

BTreeRow::DeleteRecord+1713 [ @ 0+0x0 

IndexDataSetSession::DeleteRow+296 [ @ 0+0x0 

RowsetNewSS::DeleteRows+4e [ @ 0+0x0 

CMEDScan::DeleteRow+82 [ @ 0+0x0 

CMEDCatKatmaiIndex::DropRowset+2df [ @ 0+0x0 

VisibleHoBt::DropHoBt+357 [ @ 0+0x0 SEDropRowsetInternal+68f [ @ 0+0x0 

DDLAgent::SEDropRowsets+4b [ @ 0+0x0 

CIndexDDL::DropRowset+72 [ @ 0+0x0 

CIndexDDL::DropAllRowsets+ae [ @ 0+0x0 

CIndexDDL::DropIndexEntryAndRowsets+1c0 [ @ 0+0x0 

CIndexDDL::ConstructIndicesOffline+539 [ @ 0+0x0

Conclusion

This was hard work and a steep learning curve but worthwhile. Installing the debug symbols in a development environment allows low level diagnostics that help by putting a context to events. Despite the cryptic nature of the results, I think there is value in it.

References

Ideas for a test lab - http://www.brentozar.com/archive/2013/01/sql-server-table-partitioning-tutorial-videos-and-scripts/

Clarify range left/range right partitioning - http://www.sqlskills.com/blogs/kimberly/clarifying-left-and-right-in-the-defintion-of-a-partition-function-in-sql-server-2005/

Extended Events Session & Debug Symbols - http://www.sqlskills.com/blogs/paul/determine-causes-particular-wait-type/

Partitioning Query, modified to include scheme to qualify tables and an outer join to file groups so empty ones are included - http://davidpeterhansen.com/view-partitions-in-sql-server/

Lab Setup

The T-SQL code below is the partitioning lab setup script used to generate the events, not the XE session to capture the call stack though. That and the process for downloading the SQL Server debug symbols are in Paul Randal's blog in this documents references. His XE session predicate captured only WRITE_COMPLETION wait types, that was changed here and the call stack was captured during 'file-written' events, and just for the SPID running the final INDEX REBUILD commands.

-----------------------------------------------------------------------------------  
-- PartitionLabSetup_20140428.sql
-- Creates a test database (workspace)  
-- Adds file groups and files  
-- Creates partition functions and schema's  
-- Creates and populates 2 partitioned tables (PartitionedRight & PartitionedLeft)  
-- Creates target tables for switching out  
-- Performs SWITCH & MERGE RANGE  
-----------------------------------------------------------------------------------   
USE [master]  
GO  
  -----------------------------------------------------------------------------------  
-- Create Test Database  
-----------------------------------------------------------------------------------   
:SETVAR DatabaseName "workspace"
:SETVAR TableNameRight "PartitionedRight"
:SETVAR TableNameLeft "PartitionedLeft"
:SETVAR DataDrive "D:\SQL\Data\"  
:SETVAR LogDrive "D:\SQL\Logs\"   
  
-- Drop if exists and create Database  
IF DATABASEPROPERTYEX(N'$(databasename)','Status') IS NOT NULL  
BEGIN  
    ALTER DATABASE $(DatabaseName) SET SINGLE_USER WITH ROLLBACK IMMEDIATE  
    DROP DATABASE $(DatabaseName)  
END  
GO   
  
CREATE DATABASE $(DatabaseName)  
ON  
    ( NAME = $(DatabaseName)_data,  
    FILENAME = N'$(DataDrive)$(DatabaseName)_data.mdf',  
    SIZE = 10,  
    MAXSIZE = 500,  
    FILEGROWTH = 5 )  
    LOG ON  
    ( NAME = $(DatabaseName)_log,  
    FILENAME = N'$(LogDrive)$(DatabaseName).ldf',  
    SIZE = 5MB,  
    MAXSIZE = 5000MB,  
    FILEGROWTH = 5MB ) ;   
  
GO   
  
DECLARE @msg varchar(200) = 'Run Status';  
SET @msg = 'Database $(DatabaseName) has been created';  
RAISERROR (@msg,0,0);   
  
-----------------------------------------------------------------------------------  
-- Add file groups and files  
-----------------------------------------------------------------------------------  
DECLARE @nSQL NVARCHAR(2000) ;  
DECLARE @x INT = 1;   
  
-- Range Right  
WHILE @x <= 6  
BEGIN   
  
SELECT @nSQL =  
'ALTER DATABASE $(DatabaseName)  
ADD FILEGROUP $(TableNameRight)_fg' + RTRIM(CAST(@x AS CHAR(5))) + ';   
  
ALTER DATABASE $(DatabaseName)  
ADD FILE  
(  
NAME= ''$(TableNameRight)_f' + CAST(@x AS CHAR(5)) + ''',  
FILENAME = ''$(DataDrive)\$(TableNameRight)_f' + RTRIM(CAST(@x AS CHAR(5))) + '.ndf''  
)  
TO FILEGROUP $(TableNameRight)_fg' + RTRIM(CAST(@x AS CHAR(5))) + ';'   
  
EXEC sp_executeSQL @nSQL;   
  
SET @x = @x + 1;  
END   
  
-- Range Left  
SET @x = 1  
WHILE @x <= 6  
BEGIN   
  
SELECT @nSQL =  
'ALTER DATABASE $(DatabaseName)  
ADD FILEGROUP $(TableNameLeft)_fg' + RTRIM(CAST(@x AS CHAR(5))) + ';   
  
ALTER DATABASE $(DatabaseName)  
ADD FILE  
(  
NAME= ''$(TableNameLeft)_f' + CAST(@x AS CHAR(5)) + ''',  
FILENAME = ''$(DataDrive)\$(TableNameLeft)_f' + RTRIM(CAST(@x AS CHAR(5))) + '.ndf''  
)  
TO FILEGROUP $(TableNameLeft)_fg' + RTRIM(CAST(@x AS CHAR(5))) + ';'   
  
EXEC sp_executeSQL @nSQL;   
  
SET @x = @x + 1;  
END   
  
SET @msg = 'File Groups have been created.';  
RAISERROR (@msg,0,0);   
  
-----------------------------------------------------------------------------------  
-- Create partition functions and schemes  
-----------------------------------------------------------------------------------  
USE $(DatabaseName);   
  
CREATE PARTITION FUNCTION $(TableNameRight)_func (int)  
AS RANGE RIGHT FOR VALUES  
(  
    1,  
    1001,  
    2001,  
    3001,  
    4001   
);   
  
CREATE PARTITION FUNCTION $(TableNameLeft)_func (int)  
AS RANGE LEFT FOR VALUES  
(  
    0,  
    1000,  
    2000,  
    3000,  
    4000   
);   
  
CREATE PARTITION SCHEME $(TableNameRight)_scheme  
AS  
PARTITION $(TableNameRight)_func  
TO  
(  
    $(TableNameRight)_fg1,  
    $(TableNameRight)_fg2,  
    $(TableNameRight)_fg3,  
    $(TableNameRight)_fg4,  
    $(TableNameRight)_fg5,  
    $(TableNameRight)_fg6  
);   
  
CREATE PARTITION SCHEME $(TableNameLeft)_scheme  
AS  
PARTITION $(TableNameLeft)_func  
TO  
(  
    $(TableNameLeft)_fg1,  
    $(TableNameLeft)_fg2,  
    $(TableNameLeft)_fg3,  
    $(TableNameLeft)_fg4,  
    $(TableNameLeft)_fg5,  
    $(TableNameLeft)_fg6  
);   
  
-----------------------------------------------------------------------------------  
-- Create and populate test tables  
-----------------------------------------------------------------------------------  
-- Range Right  
CREATE TABLE [dbo].$(TableNameRight)(  
    [Partition_PK] [int] IDENTITY(1,1)NOT NULL,  
    [CreateDate] [datetime] NULL,  
    [CreateServer] [nvarchar](50) NULL,  
    [RandomNbr] [int] NULL,  
    CONSTRAINT [PK_$(TableNameRight)] PRIMARY KEY CLUSTERED  
    (  
        [Partition_PK] ASC 
    ) ON $(TableNameRight)_scheme(Partition_PK)  
) ON $(TableNameRight)_scheme(Partition_PK);   
  
ALTER TABLE [dbo].$(TableNameRight) ADD CONSTRAINT [DF_$(TableNameRight)_CreateDate] DEFAULT (getdate()) FOR [CreateDate]  
ALTER TABLE [dbo].$(TableNameRight) ADD CONSTRAINT [DF_$(TableNameRight)_CreateServer] DEFAULT (@@servername) FOR [CreateServer]   
  
-- Range Left  
CREATE TABLE [dbo].$(TableNameLeft)(  
    [Partition_PK] [int] IDENTITY(1,1) NOT NULL,  
    [CreateDate] [datetime] NULL,  
    [CreateServer] [nvarchar](50) NULL,  
    [RandomNbr] [int] NULL,  
    CONSTRAINT [PK_$(TableNameLeft)] PRIMARY KEY CLUSTERED  
    (  
        [Partition_PK] ASC  
    ) ON $(TableNameLeft)_scheme(Partition_PK)  
) ON $(TableNameLeft)_scheme(Partition_PK);   
  
ALTER TABLE [dbo].$(TableNameLeft) ADD CONSTRAINT [DF_$(TableNameLeft)_CreateDate] DEFAULT (getdate()) FOR [CreateDate]  
ALTER TABLE [dbo].$(TableNameLeft) ADD CONSTRAINT [DF_$(TableNameLeft)_CreateServer] DEFAULT (@@servername) FOR [CreateServer]  
GO 
 
-- Populate test tables with different test data  
SET NOCOUNT ON;  
INSERT INTO [dbo].$(TableNameRight) (RandomNbr)  
VALUES (ROUND((RAND() * 100),0))  ;
GO 4000
 
-- Make test easier using row counts to cross reference data movement between file groups
DELETE FROM [dbo].$(TableNameRight)
WHERE Partition_PK IN (50,1050,1051,2050,2051,2052,3050,3051,3052,3053)
 
SET IDENTITY_INSERT [dbo].$(TableNameLeft) ON
INSERT INTO [dbo].$(TableNameLeft) (Partition_PK,RandomNbr)
SELECT Partition_PK, RandomNbr FROM [dbo].$(TableNameRight);
GO
  
-- Display Results Procedure
CREATE PROCEDURE dbo.DisplayTestFileGroups AS 
SELECT  
--     GETDATE() AS CaptureDate  
     fg.name AS FileGroupName  
    ,x1.PartitionNumber
    ,x1.destination_data_space_id 
    ,x1.rows
    ,x1.boundary_value_on_right AS BoundaryRight  
    ,x1.BoundaryValue  
    ,x1.first_page
    ,x1.total_pages
    ,x1.used_pages
    --,df.name AS DataFileName  
    --,df.physical_name  
    --,x1.*  
FROM sys.filegroups fg  
INNER JOIN sys.database_files df  
    ON fg.data_space_id = df.data_space_id  
LEFT OUTER JOIN  
(  
    SELECT  
         o.name ObjectName  
        ,s.name AS SchemeName  
        ,p.partition_number PartitionNumber  
        ,p.rows  
        ,i.name IndexName  
        ,au.total_pages  
        ,au.used_pages  
        ,CASE au.first_page  
            WHEN NULL THEN ''  
            ELSE CONVERT (VARCHAR(6), CONVERT (INT, SUBSTRING (au.first_page, 6, 1) +  
                    SUBSTRING (au.first_page, 5, 1))) + ':' + CONVERT (VARCHAR(20),  
                    CONVERT (INT, SUBSTRING (au.first_page, 4, 1) +  
                    SUBSTRING (au.first_page, 3, 1) + SUBSTRING (au.first_page, 2, 1) +  
                    SUBSTRING (au.first_page, 1, 1)))  
         END AS first_page  
        ,ps.name PartitionSchemeName  
        ,pf.name PartitionFunctionName  
        ,pf.boundary_value_on_right  
        ,rv.value BoundaryValue  
        ,dds.data_space_id AS destination_data_space_id  
    FROM sys.partitions p  
    INNER JOIN sys.indexes i  
         ON p.object_id = i.object_id  
        AND p.index_id = i.index_id  
    INNER JOIN sys.objects o  
         ON p.object_id = o.object_id  
    INNER JOIN sys.schemas s  
        ON s.schema_id = o.schema_id  
    INNER JOIN sys.system_internals_allocation_units au  
         ON p.partition_id = au.container_id  
    INNER JOIN sys.partition_schemes ps  
         ON ps.data_space_id = i.data_space_id  
    INNER JOIN sys.partition_functions pf  
         ON pf.function_id = ps.function_id  
    INNER JOIN sys.destination_data_spaces dds  
         ON dds.partition_scheme_id = ps.data_space_id  
         AND dds.destination_id = p.partition_number  
    LEFT OUTER JOIN sys.partition_range_values rv  
         ON pf.function_id = rv.function_id  
         AND p.partition_number = rv.boundary_id  
    WHERE i.index_id < 2  
    AND p.index_id < 2  
) x1  
    ON x1.destination_data_space_id = fg.data_space_id  
WHERE fg.name LIKE '%_fg%'
ORDER BY fg.name
GO
 
-- Show partitions and file group details
EXEC DisplayTestFileGroups;
GO
    
-----------------------------------------------------------------------------------  
-- Target tables for SWITCH of old archive data. 
  
IF OBJECT_ID('dbo.$(TableNameRight)Out') IS NOT NULL  
DROP TABLE [dbo].[$(TableNameRight)Out]   
  
CREATE TABLE [dbo].[$(TableNameRight)Out](  
    [Partition_PK] [int] NOT NULL,  
    [CreateDate] [datetime] NULL,  
    [CreateServer] [nvarchar](50) NULL,  
    [RandomNbr] [int] NULL,  
    CONSTRAINT [PK_$(TableNameRight)Out] PRIMARY KEY CLUSTERED  
    (  
        [Partition_PK] ASC  
    )  
) ON $(TableNameRight)_fg2;   
  
IF OBJECT_ID('dbo.$(TableNameLeft)Out') IS NOT NULL  
DROP TABLE [dbo].[$(TableNameLeft)Out]   
  
CREATE TABLE [dbo].[$(TableNameLeft)Out](  
    [Partition_PK] [int] NOT NULL,  
    [CreateDate] [datetime] NULL,  
    [CreateServer] [nvarchar](50) NULL,  
    [RandomNbr] [int] NULL,  
    CONSTRAINT [PK_$(TableNameLeft)Out] PRIMARY KEY CLUSTERED  
    (  
        [Partition_PK] ASC 
    )  
) ON $(TableNameLeft)_fg2;   
  
-----------------------------------------------------------------------------------  
-- Switch out partitions
----------------------------------------------------------------------------------    
ALTER TABLE dbo.$(TableNameRight)  
SWITCH PARTITION 2 TO dbo.$(TableNameRight)Out;   
  
ALTER TABLE dbo.$(TableNameLeft)  
SWITCH PARTITION 2 TO dbo.$(TableNameLeft)Out;   
 
EXEC DisplayTestFileGroups;
 
ALTER INDEX ALL ON $(TableNameRight) REBUILD;
ALTER INDEX ALL ON $(TableNameLeft) REBUILD;
CHECKPOINT;
DBCC DROPCLEANBUFFERS;
  
----------------------------------------------------------------------------------  
-- Merge 2 empty partitions at start of ranges
-- Start the 'Investigate Waits' XE session before these final index rebuilds
----------------------------------------------------------------------------------  
:SETVAR TableNameRight "PartitionedRight"
ALTER PARTITION FUNCTION $(TableNameRight)_func()  
MERGE RANGE (1001); 
:SETVAR TableNameRight "PartitionedRight"
ALTER INDEX ALL ON $(TableNameRight) REBUILD;
:SETVAR TableNameLeft "PartitionedLeft"
ALTER PARTITION FUNCTION $(TableNameLeft)_func()  
MERGE RANGE (0);    
 
:SETVAR TableNameLeft "PartitionedLeft"
ALTER INDEX ALL ON $(TableNameLeft) REBUILD;
 
EXEC DisplayTestFileGroups;

Rate

4.4 (5)

You rated this post out of 5. Change rating

Share

Share

Rate

4.4 (5)

You rated this post out of 5. Change rating