<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://blogs.msdn.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Craig Freedman's SQL Server Blog : Partitioned Tables</title><link>http://blogs.msdn.com/craigfr/archive/tags/Partitioned+Tables/default.aspx</link><description>Tags: Partitioned Tables</description><dc:language>en</dc:language><generator>CommunityServer 2.1 SP1 (Build: 61025.2)</generator><item><title>Dynamic Partition Elimination Performance</title><link>http://blogs.msdn.com/craigfr/archive/2008/08/22/dynamic-partition-elimination-performance.aspx</link><pubDate>Sat, 23 Aug 2008 01:42:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:8889031</guid><dc:creator>craigfr</dc:creator><slash:comments>1</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/8889031.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=8889031</wfw:commentRss><description>In &lt;A title="Partitioned Tables in SQL Server 2008" href="http://blogs.msdn.com/craigfr/archive/2008/07/15/partitioned-tables-in-sql-server-2008.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2008/07/15/partitioned-tables-in-sql-server-2008.aspx"&gt;this post&lt;/A&gt; on partitioned tables, I mentioned that SQL Server 2008 has a much more efficient implementation of dynamic partition elimination as compared to SQL Server 2005.&amp;nbsp; In response, a reader posted &lt;A href="http://blogs.msdn.com/craigfr/archive/2008/07/15/partitioned-tables-in-sql-server-2008.aspx#8876999" mce_href="http://blogs.msdn.com/craigfr/archive/2008/07/15/partitioned-tables-in-sql-server-2008.aspx#8876999"&gt;this comment&lt;/A&gt; asking how much dynamic partition elimination really costs in SQL Server 2005.&amp;nbsp; While I was sure that the answer is not much, I nonetheless decided to measure it and find out. 
&lt;P&gt;I wrote the following stored procedure which creates a table with the specified number of partitions:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE PROCEDURE CreateTable @N INT&lt;BR&gt;AS&lt;BR&gt;BEGIN&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; DECLARE @Cmd VARCHAR(8000)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; DECLARE @I INT&lt;BR&gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET @Cmd = 'CREATE PARTITION FUNCTION PF(INT) AS RANGE FOR VALUES (1'&lt;BR&gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET @I = 2&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; WHILE @I &amp;lt; @N&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; BEGIN&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET @Cmd = @Cmd + ',' + CONVERT(VARCHAR(4),@I)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET @I = @I + 1&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; END&lt;BR&gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET @Cmd = @Cmd + ')'&lt;BR&gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; EXECUTE (@Cmd)&lt;BR&gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY])&lt;BR&gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CREATE TABLE T1 (A INT, B INT)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CREATE CLUSTERED INDEX T1A ON T1(A) ON PS(A)&lt;BR&gt;END&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;I then created a table with either 2, 10, 100, or 1000 partitions.&amp;nbsp; For each table, I ran the following batch:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SET NOCOUNT ON&lt;BR&gt;&lt;BR&gt;DECLARE @T DATETIME&lt;BR&gt;DECLARE @I INT, @J INT, @K INT&lt;BR&gt;SET @I = 0&lt;BR&gt;SET @J = 0&lt;BR&gt;SET @T = GETDATE()&lt;BR&gt;WHILE @J &amp;lt; 10000&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; BEGIN&lt;BR&gt;&lt;B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SELECT @K = B FROM T1 WHERE A &amp;lt; @I&lt;BR&gt;&lt;/B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET @J = @J + 1&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; END&lt;BR&gt;SELECT DATEDIFF (MS, @T, GETDATE())&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;The main SELECT statement in this batch scans a single partition but, because the WHERE clause references a variable, SQL Server does not know this information at compile time.&amp;nbsp; This query is too fast to measure a single execution accurately.&amp;nbsp; Thus, I magnify the cost by repeating the query 10,000 times in a WHILE loop.&amp;nbsp; The SET NOCOUNT ON statement and the assignment in the SELECT statement merely serve to minimize client communication costs which would otherwise dominate the batch cost.&lt;/P&gt;
&lt;P&gt;The table is empty which makes this test a worst case scenario.&amp;nbsp; The cost of the query is dominated by the cost of locating the correct partition.&lt;/P&gt;
&lt;P&gt;Looking at the SQL Server 2005 STATISTICS PROFILE output for the 1000 partition table, we can see that SQL Server tests each partition before scanning only the one that is not eliminated:&lt;/P&gt;&lt;PRE&gt;0      1        |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PARTITION ID:([PtnIds1004]))
1      1             |--Filter(WHERE:([PtnIds1004]&amp;lt;=RangePartitionNew([@I],(0),(1),(2),(3),...
1000   1             |    |--Constant Scan(VALUES:(((1)),((2)),((3)),...
0      1             |--Clustered Index Seek(OBJECT:([T1].[T1A]), SEEK:([T1].[A] &amp;lt; [@I]) ORDERED FORWARD PARTITION ID:([PtnIds1004]))&lt;/PRE&gt;
&lt;P&gt;By comparison, SQL Server 2008 seeks directly to the correct partition:&lt;/P&gt;&lt;PRE&gt;0      1        |--Clustered Index Seek(OBJECT:([T1].[T1A]), SEEK:([PtnId1000] &amp;gt;= (1) AND [PtnId1000] &amp;lt;= RangePartitionNew([@I],(0),(1),(2),(3),...&lt;/PRE&gt;
&lt;P&gt;Here are the results.&amp;nbsp; In each case, before collecting any data, I ran the batch once to eliminate compilation overhead and warm up costs.&amp;nbsp; I then took the median of 5 runs.&amp;nbsp; Times are in milliseconds for the full 10,000 executions of the query.&amp;nbsp; Note that we cannot compare the SQL Server 2005 results directly to the SQL Server 2008 results as I ran these tests on two different machines.&amp;nbsp; However, the relative cost of adding partitions in SQL Server 2005 vs. SQL Server 2008 is clear.&lt;/P&gt;
&lt;TABLE class="" cellSpacing=0 cellPadding=0 width=328 border=1&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD class="" vAlign=bottom noWrap width=90&gt;
&lt;P align=right&gt;&lt;B&gt;# Partitions&lt;/B&gt;&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;&lt;B&gt;SQL Server 2005&lt;/B&gt;&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;&lt;B&gt;SQL Server 2008&lt;/B&gt;&lt;/P&gt;&lt;/TD&gt;&lt;/TR&gt;
&lt;TR&gt;
&lt;TD class="" vAlign=bottom noWrap width=90&gt;
&lt;P align=right&gt;&lt;B&gt;2&lt;/B&gt;&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;233&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;216&lt;/P&gt;&lt;/TD&gt;&lt;/TR&gt;
&lt;TR&gt;
&lt;TD class="" vAlign=bottom noWrap width=90&gt;
&lt;P align=right&gt;&lt;B&gt;10&lt;/B&gt;&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;313&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;203&lt;/P&gt;&lt;/TD&gt;&lt;/TR&gt;
&lt;TR&gt;
&lt;TD class="" vAlign=bottom noWrap width=90&gt;
&lt;P align=right&gt;&lt;B&gt;100&lt;/B&gt;&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;1216&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;216&lt;/P&gt;&lt;/TD&gt;&lt;/TR&gt;
&lt;TR&gt;
&lt;TD class="" vAlign=bottom noWrap width=90&gt;
&lt;P align=right&gt;&lt;B&gt;1000&lt;/B&gt;&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;18060&lt;/P&gt;&lt;/TD&gt;
&lt;TD class="" vAlign=bottom noWrap width=119&gt;
&lt;P align=right&gt;216&lt;/P&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;/TBODY&gt;&lt;/TABLE&gt;
&lt;P mce_keep="true"&gt;It is important to remember that these are times for 10,000 executions of the query.&amp;nbsp; Thus, even on SQL Server 2005 with 1000 partitions, the cost of a single execution of the query is only 1.8 milliseconds.&amp;nbsp; Moreover, if the table contains any real data and if the query returns even a modest number of rows, the cost of the query will be dominated by the actual data processing.&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=8889031" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Partitioned+Tables/default.aspx">Partitioned Tables</category></item><item><title>Partitioned Indexes in SQL Server 2008</title><link>http://blogs.msdn.com/craigfr/archive/2008/08/05/partitioned-indexes-in-sql-server-2008.aspx</link><pubDate>Tue, 05 Aug 2008 21:33:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:8834643</guid><dc:creator>craigfr</dc:creator><slash:comments>1</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/8834643.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=8834643</wfw:commentRss><description>&lt;P&gt;In &lt;A title="Partitioned Tables in SQL Server 2008" href="http://blogs.msdn.com/craigfr/archive/2008/07/15/partitioned-tables-in-sql-server-2008.aspx"&gt;my last post&lt;/A&gt;, I looked at how SQL Server 2008 handles scans on partitioned tables.&amp;nbsp; I explained that SQL Server 2008 treats partitioned tables as tables with a logical index on the partition id column and that SQL Server 2008 implements partition elimination by performing a logical index seek on the partition id column.&amp;nbsp; Specifically, I showed some examples using a heap.&amp;nbsp; In this post, I'll continue this discussion and explore how SQL Server 2008 handles scans and seek on partitioned indexes.&lt;/P&gt;
&lt;P&gt;Let's begin with a simple example:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE PARTITION FUNCTION PF(INT) AS RANGE FOR VALUES (0, 10, 100)&lt;BR&gt;CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY])&lt;/P&gt;
&lt;P mce_keep="true"&gt;CREATE TABLE T1 (A INT, B INT)&lt;BR&gt;CREATE CLUSTERED INDEX T1A ON T1(A) ON PS(A)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;As I noted in my last post, the SQL Server query processor logically treats this partitioned index as a multi-column index on ([PtnId], A).&amp;nbsp; We can scan this logical index just like any real index:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT * FROM T1&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Clustered Index Scan(OBJECT:([T1].[T1A]))&lt;/P&gt;
&lt;P&gt;This query uses the same plan as any other clustered index scan.&amp;nbsp; It implicitly scans all of the partitions.&amp;nbsp; Aside from the "Partitioned" attribute in the graphical and XML plans, there is no difference between this plan and a similar clustered index scan on a non-partitioned table.&amp;nbsp; We can also perform a seek on the logical index:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @I INT&lt;BR&gt;SELECT @I = 0&lt;BR&gt;SELECT * FROM T1 WHERE A = @I&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Clustered Index Seek(OBJECT:([T1].[T1A]), SEEK:(&lt;B&gt;[PtnId1000]=RangePartitionNew([@I],(0),(0),(10),(100))&lt;/B&gt; AND [T1].[A]=[@I]) ORDERED FORWARD)&lt;/P&gt;
&lt;P&gt;Notice that SQL Server derives the predicate on the [PtnId] column from the explicit predicate on column A.&amp;nbsp; SQL Server then seeks on both columns of the logical index - [PtnId] and column A - just as if the logical index were a real index.&amp;nbsp; So far, everything is pretty straightforward.&amp;nbsp; Now, let's consider a slightly more complex scenario:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @I INT&lt;BR&gt;SELECT @I = 0&lt;BR&gt;SELECT * FROM T1 &lt;B&gt;WHERE A &amp;lt; @I&lt;/B&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Both this and the prior example have predicates on column A.&amp;nbsp; In both cases, SQL Server derives a predicate on the [PtnId] column.&amp;nbsp; However, in the prior example, the derived predicate was an equality predicate that limited the seek to a single partition.&amp;nbsp; In this example, due to the inequality, the derived predicate is actually a range of partitions:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Clustered Index Seek(OBJECT:([T1].[T1A]), SEEK:(&lt;B&gt;[PtnId1000] &amp;gt;= (1) AND [PtnId1000] &amp;lt;= RangePartitionNew([@I],(0),(0),(10),(100))&lt;/B&gt; AND [T1].[A] &amp;lt; [@I]) ORDERED FORWARD)&lt;/P&gt;
&lt;P&gt;If you are familiar with &lt;A title="Seek Predicates" href="http://blogs.msdn.com/craigfr/archive/2006/07/07/652668.aspx"&gt;the rules on index seeks&lt;/A&gt;, you will notice something odd about this plan.&amp;nbsp; Ordinarily, SQL Server can only perform an index seek on the second column of a multi-column index if there exists an equality predicate on the first column.&amp;nbsp; Clearly, there is not an equality predicate on the [PtnId] column in this query and yet there is a predicate on column A.&amp;nbsp; What's going on?&amp;nbsp; The query processor supports a special type of index seek on partitioned tables.&amp;nbsp; Basically, the query processor first uses the predicate on the [PtnId] column to identify those partitions into which to seek and then seeks again using the predicate on column A.&amp;nbsp; Because the seek operator &lt;I&gt;skips&lt;/I&gt; over some rows in each partition that do not match the predicate on column A, we refer to this type of seek as a &lt;I&gt;skip scan&lt;/I&gt;.&amp;nbsp; Note that this operation really is a seek - it is not a residual predicate.&amp;nbsp; The storage engine only returns those rows that pass both predicates.&amp;nbsp; Also note that SQL Server 2008 only supports this skip scan functionality on the [PtnId] column of a partitioned table.&lt;B&gt;&lt;/B&gt;&lt;/P&gt;
&lt;P&gt;Now let's change the schema slightly:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T2 (A INT, B INT)&lt;BR&gt;CREATE CLUSTERED INDEX T2A ON T2(A) &lt;B&gt;ON PS(B)&lt;/B&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;This new table is indexed on column A but partitioned on column B.&amp;nbsp; Once again, SQL Server logically treats the index as a multi-column index on ([PtnId], B).&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @I INT&lt;BR&gt;SELECT @I = 0&lt;BR&gt;SELECT * FROM T2 WHERE A = @I&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Since the table is partitioned on column B, we cannot use the predicate on column A to derive a predicate on the [PtnId] column.&amp;nbsp; Fortunately, the query processor can still perform a skip scan by adding a predicate on the [PtnId] column that explicitly scans all partitions:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Clustered Index Seek(OBJECT:([T2].[T2A]), SEEK:([PtnId1000] &amp;gt;= (1) AND [PtnId1000] &amp;lt;= (4) AND [T2].[A]=[@I]) ORDERED FORWARD)&lt;/P&gt;
&lt;P&gt;Finally, before I wrap up this post, I'd like to point out that, while it's not often useful, we can add an explicit predicate on the [PtnId] column of a table.&amp;nbsp; To do so, we use the $PARTITION function:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @PtnId INT&lt;BR&gt;SELECT @PtnId = 1&lt;BR&gt;SELECT * FROM T1 &lt;B&gt;WHERE $PARTITION.PF(A) = @PtnId&lt;/B&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Clustered Index Scan(OBJECT:([T1].[T1A]), SEEK:(&lt;B&gt;[PtnId1000]=[@PtnId]&lt;/B&gt;) ORDERED FORWARD)&lt;/P&gt;
&lt;P&gt;Just as with the derived [PtnId] predicates, SQL Server can perform a seek on the [PtnId] column.&amp;nbsp; When using this syntax, it is important to use the correct $PARTITION function and the correct column or SQL Server will not recognize it as the [PtnId] column for this table.&lt;/P&gt;&lt;A title="Query Processing Enhancements on Partitioned Tables and Indexes" href="http://technet.microsoft.com/en-us/library/ms345599(SQL.100).aspx"&gt;Books Online&lt;/A&gt; has more information on these and other partitioned table changes in SQL Server 2008.&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=8834643" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Scans+and+Seeks/default.aspx">Scans and Seeks</category><category domain="http://blogs.msdn.com/craigfr/archive/tags/Partitioned+Tables/default.aspx">Partitioned Tables</category></item><item><title>Partitioned Tables in SQL Server 2008</title><link>http://blogs.msdn.com/craigfr/archive/2008/07/15/partitioned-tables-in-sql-server-2008.aspx</link><pubDate>Wed, 16 Jul 2008 00:03:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:8735112</guid><dc:creator>craigfr</dc:creator><slash:comments>8</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/8735112.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=8735112</wfw:commentRss><description>&lt;P&gt;In &lt;A title="Introduction to Partitioned Tables" href="http://blogs.msdn.com/craigfr/archive/2006/11/27/introduction-to-partitioned-tables.aspx"&gt;this post&lt;/A&gt;, I introduced how SQL Server 2005 implements query plans on partitioned tables.&amp;nbsp; If you've read that post or used partitioned tables, you may recall that SQL Server 2005 uses a constant scan operator to enumerate the list of partition ids that need to be scanned.&amp;nbsp; As a refresher, here is the example from that post showing the plan for scanning a table with four partitions:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE PARTITION FUNCTION PF(INT) AS RANGE FOR VALUES (0, 10, 100)&lt;BR&gt;CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY])&lt;BR&gt;CREATE TABLE T (A INT, B INT) ON PS(A)&lt;/P&gt;
&lt;P&gt;SELECT * FROM T&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PARTITION ID:([PtnIds1004]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((1)),((2)),((3)),((4))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([t]))&lt;/P&gt;
&lt;P mce_keep="true"&gt;SQL Server 2005 treats partitioned tables specially and creates special plans, such as the above one, for partitioned tables.&amp;nbsp; SQL Server 2008, on the other hand, for the most part treats partitioned tables as regular tables that just happen to be logically indexed on the partition id column.&amp;nbsp; For example, for the purposes or query optimization and query execution, SQL Server 2008 treats the above table not as a heap but as an index on [PtnId].&amp;nbsp; If we create a partitioned index (clustered or non-clustered), SQL Server 2008 logically adds the partition id column as the first column of the index.&amp;nbsp; For instance, if we create the following index:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE INDEX TA ON T(A) ON PS(A)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;SQL Server 2008 treats it not as a single column index on T(A), but rather as a multi-column or composite index on T([PtnId],A). &amp;nbsp;Note that the table and index are still stored physically as partitioned tables.&amp;nbsp; In this example, the table and non-clustered index are decomposed into four heaps and four non-clustered indexes.&lt;/P&gt;
&lt;P&gt;By treating partitioned tables as indexes, SQL Server 2008 can frequently use much simpler query plans.&amp;nbsp; Let's look at the SQL Server 2008 plan for the above example:&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;|--Table Scan(OBJECT:([T]))&lt;/P&gt;
&lt;P&gt;This query plan is no different than what you might see if you scanned any other ordinary table!&amp;nbsp; So, how can we tell that the table is really partitioned and how can we tell what partitions the plan is going to scan?&amp;nbsp; Luckily, the graphical and XML plans identify partitioned tables by adding a "Partitioned" attribute to any scan, seek, or update operator that processes a partitioned table.&amp;nbsp; Moreover, the actual graphical plan and the STATISTICS XML output identify the exact partitions that the plan touches during execution.&amp;nbsp; For example, here is an excerpt from the STATISTICS XML output generated by running the above query:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&amp;lt;RunTimePartitionSummary&amp;gt;&lt;BR&gt;&amp;nbsp; &amp;lt;PartitionsAccessed PartitionCount="4"&amp;gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;PartitionRange Start="1" End="4"/&amp;gt;&lt;BR&gt;&amp;nbsp; &amp;lt;/PartitionsAccessed&amp;gt;&lt;BR&gt;&amp;lt;/RunTimePartitionSummary&amp;gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;This output shows that, as expected, the query plan scanned all four partitions of the table.&amp;nbsp; Now, suppose we insert a row into the second partition (up until now, the table has been empty) and run the following slightly modified query:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;INSERT T VALUES (1, 1)&lt;/P&gt;
&lt;P&gt;SELECT TOP 1 * FROM T&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;This query can stop executing as soon as it finds a single row.&amp;nbsp; It scans the first partition and finding no rows continues on to scan the second partition.&amp;nbsp; At that point, it finds a row and stops.&amp;nbsp; Thus, the query terminates after scanning only two partitions.&amp;nbsp; This outcome is clearly reflected in the STATISTICS XML output:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&amp;lt;RunTimePartitionSummary&amp;gt;&lt;BR&gt;&amp;nbsp; &amp;lt;PartitionsAccessed &lt;B&gt;PartitionCount="2"&lt;/B&gt;&amp;gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;PartitionRange &lt;B&gt;Start="1" End="2"&lt;/B&gt;/&amp;gt;&lt;BR&gt;&amp;nbsp; &amp;lt;/PartitionsAccessed&amp;gt;&lt;BR&gt;&amp;lt;/RunTimePartitionSummary&amp;gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Now let's see how partition elimination works.&amp;nbsp; Let's start with static partition elimination:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT * FROM T WHERE A &amp;lt; 100&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Table Scan(OBJECT:([T]), &lt;B&gt;SEEK:([PtnId1001] &amp;gt;= (1) AND [PtnId1001] &amp;lt;= (3))&lt;/B&gt;,&amp;nbsp; WHERE:([T].[A]&amp;lt;(100)) ORDERED FORWARD)&lt;/P&gt;
&lt;P&gt;Because SQL Server 2008 treats the partitioned table like an index on [PtnId], it can simply calculate the range of partitions that need to be scanned and perform a "seek" on those partitions.&amp;nbsp; It may seem a little strange to see a SEEK predicate on a table scan, but it just means that the plan is going to scan partitions 1 through 3.&lt;/P&gt;
&lt;P&gt;And now, let's look at dynamic partition elimination:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @I INT&lt;BR&gt;SELECT @I = 0&lt;BR&gt;SELECT * FROM T WHERE A &amp;lt; @I&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;&amp;nbsp; |--Table Scan(OBJECT:([T]), SEEK:([PtnId1001] &amp;gt;= (1) AND &lt;B&gt;[PtnId1001] &amp;lt;= RangePartitionNew([@I],(0),(0),(10),(100))&lt;/B&gt;),&amp;nbsp; WHERE:([T].[A]&amp;lt;[@I]) ORDERED FORWARD)&lt;/P&gt;
&lt;P&gt;This plan is identical to the prior plan except that the constant partition id 3 has been replaced by the RangePartitionNew function which computes the correct partition id from the variable @I.&amp;nbsp; To find out exactly which partitions were scanned, we can once again check the STATISTICS XML output:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&amp;lt;RunTimePartitionSummary&amp;gt;&lt;BR&gt;&amp;nbsp; &amp;lt;PartitionsAccessed PartitionCount="1"&amp;gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;PartitionRange Start="1" End="1"/&amp;gt;&lt;BR&gt;&amp;nbsp; &amp;lt;/PartitionsAccessed&amp;gt;&lt;BR&gt;&amp;lt;/RunTimePartitionSummary&amp;gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Note that this plan computes exactly which partitions to scan.&amp;nbsp; Compare that to the SQL Server 2005 plan which evaluates a filter for each and every partition id:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PARTITION ID:([PtnIds1004]))&lt;BR&gt;&lt;B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Filter(WHERE:([PtnIds1004]&amp;lt;=RangePartitionNew([@i],(0),(0),(10),(100))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((1)),((2)),((3)),((4))))&lt;BR&gt;&lt;/B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([t]), WHERE:([t].[a]&amp;lt;[@i]) PARTITION ID:([PtnIds1004]))&lt;/P&gt;
&lt;P&gt;While evaluating the filter repeatedly may not cost too much if we have only four partitions, it is certainly going to waste some cycles if we have many partitions!&lt;/P&gt;In my next post, I'll take a look at how SQL Server 2008 handles scans and seeks on partitioned indexes.&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=8735112" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Scans+and+Seeks/default.aspx">Scans and Seeks</category><category domain="http://blogs.msdn.com/craigfr/archive/tags/Partitioned+Tables/default.aspx">Partitioned Tables</category></item><item><title>Introduction to Partitioned Tables</title><link>http://blogs.msdn.com/craigfr/archive/2006/11/27/introduction-to-partitioned-tables.aspx</link><pubDate>Mon, 27 Nov 2006 22:56:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:1161061</guid><dc:creator>craigfr</dc:creator><slash:comments>2</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/1161061.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=1161061</wfw:commentRss><description>&lt;P&gt;In this post, I’m going to take a look at how query plans involving partitioned tables work.&amp;nbsp; Note that there is a big difference between partitioned tables (available only in SQL Server 2005) and partitioned views (available both in SQL Server 2000 and in SQL Server 2005).&amp;nbsp; I will look at the query plans for partitioned views in a future post.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Table Scan&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Let’s begin by creating a simple partitioned table:&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;create&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: blue"&gt;partition&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;function&lt;/SPAN&gt; pf&lt;SPAN style="COLOR: gray"&gt;(&lt;/SPAN&gt;&lt;SPAN style="COLOR: blue"&gt;int&lt;/SPAN&gt;&lt;SPAN style="COLOR: gray"&gt;)&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;as&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;range&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;for&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;values&lt;/SPAN&gt; &lt;SPAN style="COLOR: gray"&gt;(&lt;/SPAN&gt;0&lt;SPAN style="COLOR: gray"&gt;,&lt;/SPAN&gt; 10&lt;SPAN style="COLOR: gray"&gt;,&lt;/SPAN&gt; 100&lt;SPAN style="COLOR: gray"&gt;)&lt;?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /&gt;&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;create&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: blue"&gt;partition&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;scheme&lt;/SPAN&gt; ps &lt;SPAN style="COLOR: blue"&gt;as&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;partition&lt;/SPAN&gt; pf &lt;SPAN style="COLOR: gray"&gt;all&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;to&lt;/SPAN&gt; &lt;SPAN style="COLOR: gray"&gt;(&lt;/SPAN&gt;[primary]&lt;SPAN style="COLOR: gray"&gt;)&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;create&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: blue"&gt;table&lt;/SPAN&gt; t &lt;SPAN style="COLOR: gray"&gt;(&lt;/SPAN&gt;a &lt;SPAN style="COLOR: blue"&gt;int&lt;/SPAN&gt;&lt;SPAN style="COLOR: gray"&gt;,&lt;/SPAN&gt; b &lt;SPAN style="COLOR: blue"&gt;int&lt;/SPAN&gt;&lt;SPAN style="COLOR: gray"&gt;)&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;on&lt;/SPAN&gt; ps&lt;SPAN style="COLOR: gray"&gt;(&lt;/SPAN&gt;a&lt;SPAN style="COLOR: gray"&gt;)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;This creates a table with four partitions.&amp;nbsp; SQL Server assigns sequential partition ids to each of the four partitions as follows:&lt;/P&gt;
&lt;P&gt;
&lt;TABLE class="" border=1&gt;
&lt;THEAD&gt;
&lt;TR&gt;
&lt;TH class=""&gt;PtnId&lt;/TH&gt;
&lt;TH class=""&gt;Values&lt;/TH&gt;&lt;/TR&gt;&lt;/THEAD&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TD class=""&gt;1&lt;/TD&gt;
&lt;TD class=""&gt;t.a &amp;lt;= 0&lt;/TD&gt;&lt;/TR&gt;
&lt;TR&gt;
&lt;TD class=""&gt;2&lt;/TD&gt;
&lt;TD class=""&gt;0 &amp;lt; t.a &amp;lt;= 10&lt;/TD&gt;&lt;/TR&gt;
&lt;TR&gt;
&lt;TD class=""&gt;3&lt;/TD&gt;
&lt;TD class=""&gt;10 &amp;lt; t.a &amp;lt;= 100&lt;/TD&gt;&lt;/TR&gt;
&lt;TR&gt;
&lt;TD class=""&gt;4&lt;/TD&gt;
&lt;TD class=""&gt;100 &amp;lt; t.a&lt;/TD&gt;&lt;/TR&gt;&lt;/TBODY&gt;&lt;/TABLE&gt;&lt;/P&gt;
&lt;P&gt;Now let’s examine the query plan for a simple table scan:&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: gray"&gt;*&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;from&lt;/SPAN&gt; t&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face=Tahoma size=1&gt;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PARTITION ID:([PtnIds1004]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((1)),((2)),((3)),((4))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([t]))&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;SQL Server explicitly enumerates the partition ids that the table scan must touch using the constant scan and nested loops join operators.&amp;nbsp; Recall that a nested loops join executes its second or inner input (in this case the table scan) once for each value from its first or outer input (in this case the constant scan).&amp;nbsp; Thus, we run the table scan four times; once for each partition id.&lt;/P&gt;
&lt;P&gt;Note also that the nested loops join explicitly identifies the partition id column as [PtnIds1004].&amp;nbsp; Although it is not immediately obvious from the text showplan (unfortunately, we omit this information in some though not all cases), the table scan uses the partition id column and checks it on each execution to determine which partition to scan.&amp;nbsp; This information is always available in the graphical showplan (via the table scan tool tip and properties) and in the XML showplan:&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&amp;lt;&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: maroon; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;TableScan&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: red; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;Ordered&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;=&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;"&lt;SPAN style="COLOR: blue"&gt;0&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt; &lt;/SPAN&gt;&lt;SPAN style="COLOR: red"&gt;ForcedIndex&lt;/SPAN&gt;&lt;SPAN style="COLOR: blue"&gt;=&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt;0&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt; &lt;/SPAN&gt;&lt;SPAN style="COLOR: red"&gt;NoExpandHint&lt;/SPAN&gt;&lt;SPAN style="COLOR: blue"&gt;=&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt;0&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt;&amp;gt;&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&lt;SPAN style="mso-spacerun: yes"&gt;&amp;nbsp; &lt;/SPAN&gt;...&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&lt;SPAN style="mso-spacerun: yes"&gt;&amp;nbsp; &lt;/SPAN&gt;&amp;lt;&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: maroon; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;Object&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: red; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;Database&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;=&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;"&lt;SPAN style="COLOR: blue"&gt;[master]&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt; &lt;/SPAN&gt;&lt;SPAN style="COLOR: red"&gt;Schema&lt;/SPAN&gt;&lt;SPAN style="COLOR: blue"&gt;=&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt;[dbo]&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt; &lt;/SPAN&gt;&lt;SPAN style="COLOR: red"&gt;Table&lt;/SPAN&gt;&lt;SPAN style="COLOR: blue"&gt;=&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt;[t]&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt; /&amp;gt;&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&lt;SPAN style="mso-spacerun: yes"&gt;&amp;nbsp; &lt;/SPAN&gt;&amp;lt;&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: maroon; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;PartitionId&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&amp;gt;&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&lt;SPAN style="mso-spacerun: yes"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/SPAN&gt;&amp;lt;&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: maroon; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;ColumnReference&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: red; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;Column&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;=&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;"&lt;SPAN style="COLOR: blue"&gt;PtnIds1004&lt;/SPAN&gt;"&lt;SPAN style="COLOR: blue"&gt; /&amp;gt;&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&lt;SPAN style="mso-spacerun: yes"&gt;&amp;nbsp; &lt;/SPAN&gt;&amp;lt;/&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: maroon; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;PartitionId&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&amp;gt;&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&amp;lt;/&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: maroon; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;TableScan&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;&amp;gt;&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Static Partition Elimination&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Consider the following query:&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: gray"&gt;*&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;from&lt;/SPAN&gt; t &lt;SPAN style="COLOR: blue"&gt;where&lt;/SPAN&gt; a &lt;SPAN style="COLOR: gray"&gt;&amp;lt;&lt;/SPAN&gt; 100&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face=Tahoma size=1&gt;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1005]) PARTITION ID:([PtnIds1005]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((1)),((2)),((3))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([t]), WHERE:([t].[a]&amp;lt;(100)) PARTITION ID:([PtnIds1005]))&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;The predicate “a &amp;lt; 100” clearly eliminates all rows from partition id 4.&amp;nbsp; There is no point in scanning this partition as none of the rows in this partition can possibly qualify for the predicate.&amp;nbsp; The optimizer recognizes this fact and eliminates that partition from the plan.&amp;nbsp; The constant scan now enumerates only three partitions.&amp;nbsp; We refer to this as “static partition elimination” because we know the exact list of partitions to scan statically at compile time.&lt;/P&gt;
&lt;P&gt;If we can statically eliminate all but one partition, we do not need the constant scan or join at all:&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: gray"&gt;*&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;from&lt;/SPAN&gt; t &lt;SPAN style="COLOR: blue"&gt;where&lt;/SPAN&gt; a &lt;SPAN style="COLOR: gray"&gt;&amp;lt;&lt;/SPAN&gt; 0&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face=Tahoma size=1&gt;&amp;nbsp; |--Table Scan(OBJECT:([t]), WHERE:([t].[a]&amp;lt;(0)) PARTITION ID:((1)))&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Note that the exact partition id that we must scan (partition id 1) is now part of the table scan itself.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Dynamic Partition Elimination&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;In some cases, even though it is not possible for SQL Server to determine statically at compile time which partitions need to be scanned, the optimizer may be able to determine that some partition elimination is still possible.&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;declare&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; @i &lt;SPAN style="COLOR: blue"&gt;int&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; @i &lt;SPAN style="COLOR: gray"&gt;=&lt;/SPAN&gt; 0&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: gray"&gt;*&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;from&lt;/SPAN&gt; t &lt;SPAN style="COLOR: blue"&gt;where&lt;/SPAN&gt; a &lt;SPAN style="COLOR: gray"&gt;&amp;lt;&lt;/SPAN&gt; @i&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face=Tahoma size=1&gt;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PARTITION ID:([PtnIds1004]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Filter(WHERE:([PtnIds1004]&amp;lt;=RangePartitionNew([@i],(0),(0),(10),(100))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((1)),((2)),((3)),((4))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([t]), WHERE:([t].[a]&amp;lt;[@i]) PARTITION ID:([PtnIds1004]))&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;The above query is parameterized.&amp;nbsp; Since we do not get the actual parameter value until run time (that I’ve provided a constant value for the parameter in the batch does not change things), there is no way to determine at compile time which partition ids to scan.&amp;nbsp; We may scan just partition id 1, or we might scan partition ids 1 and 2, and so forth.&amp;nbsp; Thus, the constant scan enumerates all four partition ids and we use a filter to eliminate partitions ids at run time.&amp;nbsp; We refer to this as “dynamic partition elimination.”&lt;/P&gt;
&lt;P&gt;The filter compares each partition id to the results of a special function “RangePartitionNew.”&amp;nbsp; This function computes the results of applying the partition function to the parameter value.&amp;nbsp; The arguments to this function (in left to right order) are: the value (in this case the parameter @i) that we want to map to a partition id, a Boolean flag that indicates whether the partition function maps boundary values to the left (0) or right (1), and the actual partition boundary values (in this case 0, 10, and 100).&lt;/P&gt;
&lt;P&gt;In this example, since @i has the value 0, the result of RangePartitionNew is 1.&amp;nbsp; Thus, we scan only partition id 1.&amp;nbsp; Note that unlike the static partition elimination example, even though we scan only a single partition, we still have the constant scan and join.&amp;nbsp; Again, we need these operators since we do not know how many partitions we will scan until run time.&lt;/P&gt;
&lt;P&gt;In some cases, the optimizer can determine at compile time that we will scan only one partition even though it cannot determine which partition.&amp;nbsp; For example, if we have an equality predicate on the partition key, we know that only partition can match.&amp;nbsp; In this case, even though we have dynamic partition elimination, we do not need the constant scan and join.&amp;nbsp; For example:&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;declare&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; @i &lt;SPAN style="COLOR: blue"&gt;int&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; @i &lt;SPAN style="COLOR: gray"&gt;=&lt;/SPAN&gt; 0&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: gray"&gt;*&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;from&lt;/SPAN&gt; t &lt;SPAN style="COLOR: blue"&gt;where&lt;/SPAN&gt; a &lt;SPAN style="COLOR: gray"&gt;=&lt;/SPAN&gt; @i&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face=Tahoma size=1&gt;&amp;nbsp; |--Table Scan(OBJECT:([t]), WHERE:([t].[a]=[@i]) PARTITION ID:(RangePartitionNew([@i],(0),(0),(10),(100))))&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Combining Static and Dynamic Partition Elimination&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;SQL Server can combine static and dynamic partition elimination in a single query plan:&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;declare&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; @i &lt;SPAN style="COLOR: blue"&gt;int&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; @i &lt;SPAN style="COLOR: gray"&gt;=&lt;/SPAN&gt; 0&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: gray"&gt;*&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;from&lt;/SPAN&gt; t &lt;SPAN style="COLOR: blue"&gt;where&lt;/SPAN&gt; a &lt;SPAN style="COLOR: gray"&gt;&amp;gt;&lt;/SPAN&gt; 0 &lt;SPAN style="COLOR: gray"&gt;and&lt;/SPAN&gt; a &lt;SPAN style="COLOR: gray"&gt;&amp;lt;&lt;/SPAN&gt; @i&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face=Tahoma size=1&gt;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PARTITION ID:([PtnIds1004]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Filter(WHERE:([PtnIds1004]&amp;lt;=RangePartitionNew([@i],(0),(0),(10),(100))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((2)),((3)),((4))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([t]), WHERE:([t].[a]&amp;lt;[@i] AND [t].[a]&amp;gt;(0)) PARTITION ID:([PtnIds1004]))&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Note that we statically eliminated partition id 1 from the constant scan and we may dynamically eliminate additional partition ids via a filter.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;$partition&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;You can explicitly invoke the RangePartitionNew function using $partition:&lt;/P&gt;
&lt;P class=MsoNormal style="MARGIN: 0in 0in 0pt; mso-layout-grid-align: none"&gt;&lt;SPAN style="FONT-SIZE: 10pt; COLOR: blue; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt;select&lt;/SPAN&gt;&lt;SPAN style="FONT-SIZE: 10pt; FONT-FAMILY: 'Courier New'; mso-no-proof: yes"&gt; &lt;SPAN style="COLOR: gray"&gt;*,&lt;/SPAN&gt; &lt;SPAN style="COLOR: fuchsia"&gt;$partition&lt;/SPAN&gt;&lt;SPAN style="COLOR: gray"&gt;.&lt;/SPAN&gt;pf&lt;SPAN style="COLOR: gray"&gt;(&lt;/SPAN&gt;a&lt;SPAN style="COLOR: gray"&gt;)&lt;/SPAN&gt; &lt;SPAN style="COLOR: blue"&gt;from&lt;/SPAN&gt; t&lt;o:p&gt;&lt;/o:p&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;FONT face=Tahoma size=1&gt;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1004]=RangePartitionNew([t].[a],(0),(0),(10),(100))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1005]) PARTITION ID:([PtnIds1005]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan(VALUES:(((1)),((2)),((3)),((4))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([t]))&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;This query plan is identical to the table scan plan above except that we compute and output the partition id for each row.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;For More Information&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;A class="" href="http://blogs.msdn.com/sqlcat/archive/2006/02/17/Partition-Elimination-in-SQL-Server-2005.aspx" mce_href="http://blogs.msdn.com/sqlcat/archive/2006/02/17/Partition-Elimination-in-SQL-Server-2005.aspx"&gt;This post&lt;/A&gt; from the SQL Server Development Customer Advisory Team blog includes more partition elimination examples.&amp;nbsp; The post includes some examples of where partition elimination does not work if you have predicates with disjunctions and parameters (such as “a &amp;gt; 0 or a &amp;lt; @i”) and some workarounds.&amp;nbsp; One of the workarounds is to use a “union all” statement to eliminate the disjunction.&amp;nbsp; If you use this workaround, beware that you must be sure that the or’ed predicates will not overlap and return duplicates.&amp;nbsp; In some cases, a duplicate eliminating union (be sure to include a unique key in the select list) may be safer.&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=1161061" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Scans+and+Seeks/default.aspx">Scans and Seeks</category><category domain="http://blogs.msdn.com/craigfr/archive/tags/Partitioned+Tables/default.aspx">Partitioned Tables</category></item></channel></rss>