Partition Elimination in SQL Server 2005
Lubor Kollar
Partition elimination is very important when SQL Server executes queries against partitioned tables or partitioned views. In general, SQL Server is doing an excellent job of not scanning the partitions that are excluded by some predicates. Recently we have discovered one scenario where the partition elimination does not work against partitioned tables in SQL Server 2005 and this blog describes the conditions leading to the problem as well as easy workarounds. Additionally you will learn how to discover if partition elimination works for your query or not. You will also learn what is static and dynamic partition elimination.
The most reliable way to find out if partition elimination happens in your query is to use the SET STATISTICS PROFILE ON command, run the query and investigate the output. But let me start with building our example table:
create partition function PF1 (int) as range for values (100,200,300,400);
create partition scheme PS1 as partition PF1 all to ([PRIMARY]);
go
create table t1 (a int, b int) on PS1 (a);
go
declare @i int;
set @i=1;
set nocount on;
while (@i<22)
begin;
insert into t1 values (20*@i, @i);
set @i=@i+1;
end;
go
The following query shows distribution of all rows in table t1 across the five partitions:
select $partition.PF1(a) [Partition Number], a, b from t1
Partition Number a b
1 20 1
1 40 2
1 60 3
1 80 4
1 100 5
2 120 6
2 140 7
2 160 8
2 180 9
2 200 10
3 220 11
3 240 12
3 260 13
3 280 14
3 300 15
4 320 16
4 340 17
4 360 18
4 380 19
4 400 20
5 420 21
First, I will show 5 examples how partition elimination works correctly in SQL Server 2005 and I will explain the difference between the static and dynamic partition elimination. Here is a small batch and we will investigate the output later below
set statistics profile on;
declare @i1 int;
declare @i2 int;
set @i1=50;
set @i2=250 ;
select * from t1 where a<50 or a>450; -- (Q1) only two partitions are scanned
select * from t1 where a in (50,450); -- (Q2) only two partitions are scanned
select * from t1 where a<@i2 and a>100; -- (Q3) only two partitions are scanned
select * from t1 where a=100;-- (Q4) only one partition is scanned - static partition elimination
select * from t1 where a=@i2; -- (Q5) only one partition is scanned - dynamic partition elimination
set statistics profile off;
You will see the result set followed by the showplan with the columns “Rows” and “Executes” in front of it for each of the four queries above. For the query Q1
select * from t1 where a<50 or a>450
the showplan output is
Rows Executes StmtText
2 1 select * from t1 where a<50 or a>450; -- (Q1) only two partitions
2 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PAR
2 1 |--Constant Scan(VALUES:(((1)),((5))))
2 2 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[d
The scan of the partitioned table becomes a Nested Loops looping over the partitions. And we see already in the Constant Scan that we will be scanning only partitions 1 and 5. The “Executes” value 2 below confirms we did 2 scans of an individual partition.
The IN predicate “a in (50,450)” in the Q2 is turned into “ a = 50 OR a = 450”, and SQL Server will access only the two partitions, 1, and 5, containing all qualifying rows
Rows Executes StmtText
0 1 select * from t1 where a in (50,450); -- (Q2) only two partitions are sc
0 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PAR
2 1 |--Constant Scan(VALUES:(((1)),((5))))
0 2 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[dbo].[t
The second query, Q3,
select * from t1 where a<@i2 and a>100
yields
Rows Executes StmtText
7 1 select * from t1 where a<@i2 and a>100; -- (Q3) only two partitions
7 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PAR
2 1 |--Filter(WHERE:([PtnIds1004]<=RangePartitionNew([@i2],(0)
4 1 | |--Constant Scan(VALUES:(((2)),((3)),((4)),((5))))
7 2 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[d
In the above plan we see that we have statically eliminated the partition 1 from the Constant Scan because of the a>100 predicate and we are using the predicate a<@i2 to potentially eliminate more partitions using the Filter above the Constant Scan. The later is dynamic elimination because it depends on the run time value of the @i2 how many additional partitions will be eliminated. By looking at the “Executes” of the Table Scan we see that again we are scanning only 2 individual partitions.
The query Q4
select * from t1 where a=100
has no Constant Scan at all. This is because SQL Server knows already in the compile time which single partition will be accessed to retrieve the complete result. Here is the plan
Rows Executes StmtText
1 1 SELECT * FROM [t1] WHERE [a]=@1
1 1 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[dbo]. [t1].[a]=(100)) PARTITION ID:((1)))
Now compare the above with the plan for query Q5
select * from t1 where a=@i2.
Also here we know we will scan only a single partition but we don’t know which at the time we compile the query therefore the WHERE is paramaterized
Rows Executes StmtText
0 1 select * from t1 where a=@i2; -- (Q5) only one partition is scanned 0 1 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[dbo].[t1].[a]=[@i2]) PARTITION ID:(RangePartitionNew([@i2],(0),(100),(200),(300),(400))))
All the above partition elimination worked as expected and we saw that SQL Server skipped as many partitions as possible. Now investigate the following cases
set statistics profile on;
declare @i1 int;
declare @i2 int;
set @i1=50;
set @i2=250;
select * from t1 where a<50 or a>@i2; -- (Q6)
select * from t1 where a<@i1 or a>@i2; -- (Q7)
select * from t1 where a in (@i1,@i2); -- (Q8)
set statistics profile off;
Taking into consideration the boundary values (100,200,300,400) and the values of @i1=50 and @i2=250, both the Q6 and Q7 should be able to skip the second partition safely. However, the plans are
Rows Executes StmtText
11 1 select * from t1 where a<50 or a>@i2 -- (Q6) all partitions are
11 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PAR
5 1 |--Constant Scan(VALUES:(((1)),((2)),((3)),((4)),((5))))
11 5 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[d
and
Rows Executes StmtText
11 1 select * from t1 where a<@i1 or a>@i2 -- (Q7) all partitions are
11 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PAR
5 1 |--Constant Scan(VALUES:(((1)),((2)),((3)),((4)),((5))))
11 5 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[d
The Q8 should scan only two partitions – 1 and 3 – but it will scan all five again
Rows Executes StmtText
0 1 select * from t1 where a in (@i1,@i2) -- (Q8)
0 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1004]) PAR
5 1 |--Constant Scan(VALUES:(((1)),((2)),((3)),((4)),((5))))
0 5 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[d
In both cases SQL Server 2005 is scanning all partitions. The partition elimination in SQL Server 2005 does not work if there is OR between the eliminating predicates and if at least one of them is parameterized at the same time. Since the IN predicate with at least 2 elements inside the list is transformed to an OR predicate, the same is true for the IN lists with at least one parameter value as well.
There are several possible workarounds, and I will show the most convenient one with the UNION ALL replacing the OR. In our case the query Q6 will become
select * from t1 where a<50
UNION ALL
select * from t1 where a>@i2
with the query plan
Rows Executes StmtText
11 1 select * from t1 where a<50 UNION ALL select * from t1 where a
11 1 |--Concatenation
2 1 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[d
9 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1011]
3 1 |--Filter(WHERE:([PtnIds1011]>=RangePartitionNew([@i2
5 1 | |--Constant Scan(VALUES:(((1)),((2)),((3)),((4))
9 3 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([tes
The query plan above has static elimination for the predicate a<50 and dynamic partition elimination for the predicate a>i2. The first one will end up accessing a single partition and the second will scan the last three partitions because of the @i2 value of 250.
The second query, Q6, will be rewritten to
select * from t1 where a<@i1
UNION ALL
select * from t1 where a>@i2
with the query plan
Rows Executes StmtText
11 1 select * from t1 where a<@i1 UNION ALL select * from t1 where
11 1 |--Concatenation
2 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1010]
1 1 | |--Filter(WHERE:([PtnIds1010]<=RangePartitionNew([@i1
5 1 | | |--Constant Scan(VALUES:(((1)),((2)),((3)),((4))
2 1 | |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([tes
9 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([PtnIds1011]
3 1 |--Filter(WHERE:([PtnIds1011]>=RangePartitionNew([@i2
5 1 | |--Constant Scan(VALUES:(((1)),((2)),((3)),((4))
9 3 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([tes
And the third query, Q7, is equivalent to
select * from t1 where a =@i1
UNION ALL
select * from t1 where a =@i2
with the query plan
Rows Executes StmtText
0 1 select * from t1 where a =@i1 -- (Q7) UNION ALL select * fro
0 1 |--Concatenation
0 1 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[d
0 1 |--Table Scan(OBJECT:([test].[dbo].[t1]), WHERE:([test].[d
You can also use dynamic query to remove the parameters by converting them to strings and concatenating with the rest of the query. In SQL Server 2000 and before such use of dynamic query inside of a stored procedure would cause authentication failure for users of the stored procedure without explicit access to the objects referenced in the dynamic query but in SQL Server 2005 this problem may be avoided by using the EXECUTE AS clause that can implicitly define the execution context of the statement.
Summary
I have shown how you can find out if SQL Server eliminates partitions in your queries. Most of the time SQL Server is doing great job in partition elimination and it is scanning or seeking only partitions that could potentially yield rows satisfying the WHERE, IN or other row eliminating clauses. In spite of that you should still check the query plans of your queries against partitioned tables if you suspect the performance may be or is an issue. You should be aware that SQL Server is employing both the static partition elimination when the query is optimized and dynamic partition elimination when the choice of scanned and skipped partitions is made at the query execution time.
I have shown an example, when there is at least one parameter and either IN or OR clause when SQL Server is not eliminating all partitions it could. I’m showing a workaround using a UNION ALL instead of the OR and SQL Server is again eliminating all partitions that cannot yield any row satisfying the query. SQL Server development team is planning to address the above mentioned partition elimination problem in one of the future SQL Server service packs or releases.