I have a couple of questions about the phyiscal layout of tables when they are partitioned. I've been researching this but am still a little unsure.
Say I have an existing table:-
CREATE TABLE dbo.[ExampleTable]
(ID INT IDENTITY(1,1),
Col1 SYSNAME,
Col2 SYSNAME,
CreatedDATE DATE) ON [DATA];
ALTER TABLE dbo.[ExampleData] ADD CONSTRAINT [PK_ExampleTable] PRIMARY KEY CLUSTERED
( [ID] ASC )
GO
I want to partition this table on the CreatedDate column (all partitions in the same filegroup for this example) but I cannot have the column as a Primary Key on its own. So I add the CreatedDate column to the Primary Key:-
ALTER TABLE dbo.[ExampleTable] DROP CONSTRAINT PRIMARY KEY
ALTER TABLE dbo.[ExampleTable] ADD CONSTRAINT [PK_ExampleTable] PRIMARY KEY CLUSTERED
( [ID] ASC, [CreatedDate] ASC ) ON PartitionScheme(CreatedDate)
GO
My question is how will data will be sorted? Will the data be physically split into partitions by the CreatedDate column and then ordered by the ID column? Or are the partitions logical and the data remains ordered by the ID column?
Also, what would happen if the ID column was a GUID? Would the data be in partitions then then horribly fragmented within those partitions?
Any advice would be most appreciated, thank you.
Andrew
EDIT:- Adding in the Partition Scheme and function:-
DECLARE @CurrentDate DATETIME;
CREATE PARTITION FUNCTION PF_Example (DATETIME)
AS RANGE RIGHT
FOR VALUES (@CurrentDate+7,@CurrentDate+6,@CurrentDate+5,@CurrentDate+4,
@CurrentDate+3,@CurrentDate+2,@CurrentDate+1,@CurrentDate,
@CurrentDate-1,@CurrentDate-2,@CurrentDate-3,@CurrentDate-4,
@CurrentDate-5,@CurrentDate-6,@CurrentDate-7,@CurrentDate-8);
CREATE PARTITION SCHEME PS_Example
AS PARTITION PF_Example
ALL TO (Data);
Best Answer
Ok, so here is a quick example demonstrating why - in the case where most of your operations (reporting queries, archive operations, partition switches etc.) will identify ranges of rows by date - you're better off clustering on the partitioning column. Let's have a simple date-based partition scheme and function:
Then two tables - one with a clustered PK on ID, Date and a non-clustered index on Date, and another with a non-clustered PK on ID, Date and a clustered index on Date.
Now fill them with some data:
So we should have 100 rows in partition 1, and 50 rows in partition 2, right?
sys.partitions
confirms:Results:
Notice that in both cases the data in the PK is all stored in a single partition. How does that affect queries? Well, consider these four, which are probably typical (aside from the
SELECT *
, used only for brevity):Here are some results from SQL Sentry Plan Explorer:*
Estimated costs and actual runtime metrics:
The
SELECT *
against the non-clustered PK performed an efficient clustered index seek, accessing only a single partition:When the PK is clustered, it decides instead to perform a clustered index scan, which means it can't eliminate partitions, leading to more reads and therefore a higher I/O cost. Interesting to note, also, that the scan is not ordered.
Similar things happen with the delete. The most expensive part of the delete operation in both cases is the clustered index delete; having the benefit of partition elimination makes the non-clustered PK much more desirable for supporting this operation (even though ultimately the reads required and up being about the same).
With the clustered PK, the source rows are found with a seek (which you might expect to be more efficient), but again the most work is performed by the subsequent delete, so at least at this size it doesn't have much impact at all:
Now, at much higher volumes, that leading scan may wind up tipping the scale in the other direction, so you're going to have to test.
Of course at this low end this has a negative impact on single-row queries where you identify by ID, since you will typically identify the row by an index seek and then have to do a lookup, vs. a single clustered index seek. Let's consider these two queries (again, regarding
SELECT *
, do as I say, not as I do):Results from Plan Explorer:
The first one is simple, it just needs a clustered index seek (and therefore no lookups):
But as mentioned, the second one decides on a non-partitioned seek against the PK, but a partitioned key lookup. In this case that ends up being more expensive, but might not always, and might not always be the optimizer's choice, either.
The same kind of thing may happen with certain join queries, depending on how many rows and how the join is constructed.
And again, the optimizer's choices here are often going to be volume-dependent. So, in the end: it depends. My choice with the information you've provided would be to cluster on the partitioning key and use a non-clustered PK. And I would strongly avoid using a GUID for this ID in either case - while that distribution might be good for inserts if you're trying to insert 8 billion rows a second, it's not going to help for anything else you're doing.
Another option is to use a single, combined PK on Date first, then ID:
This obviously results in fewer rows being stored on fewer pages (no non-clustered index to maintain, for example):
Results:
But how does it affect these other queries? The
SELECT *
is identical to theSELECT *
on the non-clustered PK version; a simple clustered index seek. TheDELETE
, however, is a much simpler plan:The single-row seek, however, ends up being much more expensive:
You can probably combat that with a non-clustered covering index on ID, which would convert the scan to a seek (with a lookup if the index is non-covering), but would still not benefit from partition elimination.
*
Disclaimer: I work for SQL Sentry.