SQL Server query scanning more partitions than expectedPartition Key questions in SQL Server 2008db2 range partitioning attach and detach issueWhy won't Oracle alter the size of a column that is used for sub partitioning?SQL Partioning on date, how does it work for future records?effective mysql table/index design for 35 million rows+ table, with 200+ corresponding columns (double), any combination of which may be queriedWhy can an Oracle SET UNUSED COLUMN cause internal UPDATE SET X=X and huge log generation?Getting a realistic query plan when partitioning with low volumes of dataHow to maintain a global index on a large table undergoing regular partition purges?SQL Error: ORA-14300: While partitioning and subpartitioningSQL Server 2014 Transactional Replication Partition SwitchingMerge replication - corrupt row continually trying to INSERT
The space of cusp forms for GL_2 over F_q(T)
How to mock ApexTestQueueItem, AsyncApexJob, and ApexTestResult for test coverage?
A Magic Diamond
Attacking the Hydra
Airline power sockets shut down when I plug my computer in. How can I avoid that?
What is the question mark?
Unconventional examples of mathematical modelling
How does the Moon's gravity affect Earth's oceans despite Earth's stronger gravitational pull?
What exactly happened to the 18 crew members who were reported as "missing" in "Q Who"?
Why is the battery jumpered to a resistor in this schematic?
Why does "auf der Strecke bleiben" mean "to fall by the wayside"?
Deciphering Lunacy Asylum case notes about General appearance / Description of Bodily Symptoms
What allows us to use imaginary numbers?
Can anybody tell me who this Pokemon is?
What is the opposite of "hunger level"?
Is there a way, other than having a Diviner friend, for a player to avoid rolling Initiative at the start of a combat?
Have there ever been other TV shows or Films that told a similiar story to the new 90210 show?
Units of measurement, especially length, when body parts vary in size among races
Why do we use low resistance cables to minimize power losses?
What are some tips and tricks for finding the cheapest flight when luggage and other fees are not revealed until far into the booking process?
Java methods to add and authenticate users in MySQL
String routines
Would getting a natural 20 with a penalty still count as a critical hit?
Who owns content posted at Paizo.com forums?
SQL Server query scanning more partitions than expected
Partition Key questions in SQL Server 2008db2 range partitioning attach and detach issueWhy won't Oracle alter the size of a column that is used for sub partitioning?SQL Partioning on date, how does it work for future records?effective mysql table/index design for 35 million rows+ table, with 200+ corresponding columns (double), any combination of which may be queriedWhy can an Oracle SET UNUSED COLUMN cause internal UPDATE SET X=X and huge log generation?Getting a realistic query plan when partitioning with low volumes of dataHow to maintain a global index on a large table undergoing regular partition purges?SQL Error: ORA-14300: While partitioning and subpartitioningSQL Server 2014 Transactional Replication Partition SwitchingMerge replication - corrupt row continually trying to INSERT
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty margin-bottom:0;
So my team has the following select statement in one of our stored procedures
SELECT
ai.Name
,dc.Component
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 + ai.DayOfMonth - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 + ai.DayOfMonth - 1)))
GROUP BY
Name
,Component
And we've partitioned the DailyCosts table on the months of each year as follows:
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
...
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
We noticed that when we ran the procedure, rather than finding the appropriate partitions right away it looked like it was scanning partitions from either end. For the above example, it viewed partitions 20180101
up to 20190701
and 20301201
down to 20270601
totaling 62 partitions.
When we removed the math from the where statement (+ ai.DayOfMonth - 1
), the partitions read dropped down to 2, 20190601
and 20270601
, as expected. Note that we use seed data and DayOfMonth
is set to 15 on every account.
What causes the server to scan the partitions when this math is included, and is it actually looking through all of the indexes in these partitions or is it just checking their ranges and moving on?
Any and all sources you can provide would go a long way in helping us understand!
sql-server partitioning
New contributor
add a comment |
So my team has the following select statement in one of our stored procedures
SELECT
ai.Name
,dc.Component
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 + ai.DayOfMonth - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 + ai.DayOfMonth - 1)))
GROUP BY
Name
,Component
And we've partitioned the DailyCosts table on the months of each year as follows:
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
...
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
We noticed that when we ran the procedure, rather than finding the appropriate partitions right away it looked like it was scanning partitions from either end. For the above example, it viewed partitions 20180101
up to 20190701
and 20301201
down to 20270601
totaling 62 partitions.
When we removed the math from the where statement (+ ai.DayOfMonth - 1
), the partitions read dropped down to 2, 20190601
and 20270601
, as expected. Note that we use seed data and DayOfMonth
is set to 15 on every account.
What causes the server to scan the partitions when this math is included, and is it actually looking through all of the indexes in these partitions or is it just checking their ranges and moving on?
Any and all sources you can provide would go a long way in helping us understand!
sql-server partitioning
New contributor
add a comment |
So my team has the following select statement in one of our stored procedures
SELECT
ai.Name
,dc.Component
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 + ai.DayOfMonth - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 + ai.DayOfMonth - 1)))
GROUP BY
Name
,Component
And we've partitioned the DailyCosts table on the months of each year as follows:
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
...
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
We noticed that when we ran the procedure, rather than finding the appropriate partitions right away it looked like it was scanning partitions from either end. For the above example, it viewed partitions 20180101
up to 20190701
and 20301201
down to 20270601
totaling 62 partitions.
When we removed the math from the where statement (+ ai.DayOfMonth - 1
), the partitions read dropped down to 2, 20190601
and 20270601
, as expected. Note that we use seed data and DayOfMonth
is set to 15 on every account.
What causes the server to scan the partitions when this math is included, and is it actually looking through all of the indexes in these partitions or is it just checking their ranges and moving on?
Any and all sources you can provide would go a long way in helping us understand!
sql-server partitioning
New contributor
So my team has the following select statement in one of our stored procedures
SELECT
ai.Name
,dc.Component
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 + ai.DayOfMonth - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 + ai.DayOfMonth - 1)))
GROUP BY
Name
,Component
And we've partitioned the DailyCosts table on the months of each year as follows:
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
...
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
We noticed that when we ran the procedure, rather than finding the appropriate partitions right away it looked like it was scanning partitions from either end. For the above example, it viewed partitions 20180101
up to 20190701
and 20301201
down to 20270601
totaling 62 partitions.
When we removed the math from the where statement (+ ai.DayOfMonth - 1
), the partitions read dropped down to 2, 20190601
and 20270601
, as expected. Note that we use seed data and DayOfMonth
is set to 15 on every account.
What causes the server to scan the partitions when this math is included, and is it actually looking through all of the indexes in these partitions or is it just checking their ranges and moving on?
Any and all sources you can provide would go a long way in helping us understand!
sql-server partitioning
sql-server partitioning
New contributor
New contributor
New contributor
asked 8 hours ago
BenBen
161 bronze badge
161 bronze badge
New contributor
New contributor
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
Reasoning
When comparing the column of the non partitioned table against the partitioned table, sql server will not be able to know what DayOfMonth
will hold, even if all of them are 15.
As a result it would not know which partitions to return to satisfy this filtering when joining the two tables.
A different example to give some more insight can be found here.
Testing
I was able to recreate your issue, for further questions please add as much information as possible.
This could be Table definition, indexes, partition scheme, ...
The DDL & DML is at the bottom.
When running the query we can get the same result:
62 partitions returned.
On the seek predicates it tries to filter what it can, this is the two filters on CalendarId
that do not have the + DayOfMonth-1
.
This translates to
WHERE ...
AND (( dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601))
...
When running the query with these filters you will see the same amount of rows returned on accessing the table in the query plan.
Only after getting this data, it can and will be joined to the AccountInfo
table on the ID
's and the DayOfMonth - 1
And to confirm when we change the query:
SELECT
ai.Name
,SUM(dc.Component )
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 - 1)))
GROUP BY Name
It does eleminate partitions:
Solution
I am going to assume that DayOfMonth
only goes up to 31
days.
If you know these boundaries, you can 'hard code' them so that sql server knows what partitions to look for. After this you can add the extra filtering.
E.G.
WITH CTE
AS
(
SELECT
ai.Name
,dc.Component
,dc.Value
,dc.CalendarId
,ai.DayOfMonth
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 )
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270701)))
)
SELECT Name,Component,SUM(Value)
FROM CTE
WHERE ((CalendarId >= (20190601 + DayOfMonth - 1)
AND CalendarId < 20190701)
OR (CalendarId >= 20270601
AND CalendarId < (20270601 + DayOfMonth - 1)))
GROUP BY
Name
,Component
The sole purpose of the cte
is to let sql server know that it can also filter on dc.CalendarId >= dc.CalendarId >= (20190601 ) OR dc.CalendarId < (20270701))
.
Sidenote: Adding a constraint does not work to enforce this.
This query gives us the result we want, with the correct partition elemination:
Only accessing partitions 19 & 21.
Test data
CREATE SCHEMA WARM
GO
CREATE TABLE Warm.DailyCosts(ID INT IDENTITY(1,1) NOT NULL,
Component int,
Value int,
CalendarId INT,
AccountInfoId int
)
CREATE TABLE Warm.AccountInfo(Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL,
Name VARCHAR(25),
CorrelationId uniqueidentifier,
DayOfMonth int
);
USE [master]
GO
ALTER DATABASE [test] ADD FILEGROUP [Partitionfg]
GO
USE [test]
GO
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
20190101,
20190201,
20190301,
20190401,
20190501,
20190601,
20190701,
20270601,
20270701,
20270801,
20270901,
20271001,
20271101,
20271201,
20280101,
20280201,
20280301,
20280401,
20280501,
20280601,
20280701,
20280801,
20280901,
20281001,
20281101,
20281201,
20290101,
20290201,
20290301,
20290401,
20290501,
20290601,
20290701,
20290801,
20290901,
20291001,
20291101,
20291201,
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
CREATE PARTITION SCHEME [PS_CostDateByYearMonth]
AS PARTITION [PF_CostDateByYearMonth]
ALL TO ( [Partitionfg] );
CREATE UNIQUE CLUSTERED INDEX ptci ON Warm.DailyCosts(CalendarId,Id) ON [PS_CostDateByYearMonth](CalendarId) ;
USE [master]
GO
ALTER DATABASE [test] ADD FILE ( NAME = N'TestPartition', FILENAME = N'D:DATATestPartition.ndf' , SIZE = 3072KB , FILEGROWTH = 65536KB ) TO FILEGROUP [Partitionfg]
GO
USE [test]
GO
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20180101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2018
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20190101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2019
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20280101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2028
INSERT INTO Warm.AccountInfo(
Name ,
CorrelationId,
DayOfMonth
)
SELECT TOP(3000000) --3M
CAST(ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS varchar(10)) + 'a',
'00000000-0000-0000-0000-000000000000',
15
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
CREATE NONCLUSTERED INDEX [IX_Component_Value_DailyCosts] ON Warm.DailyCosts
(Component,Value)
ON [PS_CostDateByYearMonth](CalendarId);
GO
add a comment |
Your Answer
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "182"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Ben is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fdba.stackexchange.com%2fquestions%2f245575%2fsql-server-query-scanning-more-partitions-than-expected%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
Reasoning
When comparing the column of the non partitioned table against the partitioned table, sql server will not be able to know what DayOfMonth
will hold, even if all of them are 15.
As a result it would not know which partitions to return to satisfy this filtering when joining the two tables.
A different example to give some more insight can be found here.
Testing
I was able to recreate your issue, for further questions please add as much information as possible.
This could be Table definition, indexes, partition scheme, ...
The DDL & DML is at the bottom.
When running the query we can get the same result:
62 partitions returned.
On the seek predicates it tries to filter what it can, this is the two filters on CalendarId
that do not have the + DayOfMonth-1
.
This translates to
WHERE ...
AND (( dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601))
...
When running the query with these filters you will see the same amount of rows returned on accessing the table in the query plan.
Only after getting this data, it can and will be joined to the AccountInfo
table on the ID
's and the DayOfMonth - 1
And to confirm when we change the query:
SELECT
ai.Name
,SUM(dc.Component )
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 - 1)))
GROUP BY Name
It does eleminate partitions:
Solution
I am going to assume that DayOfMonth
only goes up to 31
days.
If you know these boundaries, you can 'hard code' them so that sql server knows what partitions to look for. After this you can add the extra filtering.
E.G.
WITH CTE
AS
(
SELECT
ai.Name
,dc.Component
,dc.Value
,dc.CalendarId
,ai.DayOfMonth
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 )
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270701)))
)
SELECT Name,Component,SUM(Value)
FROM CTE
WHERE ((CalendarId >= (20190601 + DayOfMonth - 1)
AND CalendarId < 20190701)
OR (CalendarId >= 20270601
AND CalendarId < (20270601 + DayOfMonth - 1)))
GROUP BY
Name
,Component
The sole purpose of the cte
is to let sql server know that it can also filter on dc.CalendarId >= dc.CalendarId >= (20190601 ) OR dc.CalendarId < (20270701))
.
Sidenote: Adding a constraint does not work to enforce this.
This query gives us the result we want, with the correct partition elemination:
Only accessing partitions 19 & 21.
Test data
CREATE SCHEMA WARM
GO
CREATE TABLE Warm.DailyCosts(ID INT IDENTITY(1,1) NOT NULL,
Component int,
Value int,
CalendarId INT,
AccountInfoId int
)
CREATE TABLE Warm.AccountInfo(Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL,
Name VARCHAR(25),
CorrelationId uniqueidentifier,
DayOfMonth int
);
USE [master]
GO
ALTER DATABASE [test] ADD FILEGROUP [Partitionfg]
GO
USE [test]
GO
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
20190101,
20190201,
20190301,
20190401,
20190501,
20190601,
20190701,
20270601,
20270701,
20270801,
20270901,
20271001,
20271101,
20271201,
20280101,
20280201,
20280301,
20280401,
20280501,
20280601,
20280701,
20280801,
20280901,
20281001,
20281101,
20281201,
20290101,
20290201,
20290301,
20290401,
20290501,
20290601,
20290701,
20290801,
20290901,
20291001,
20291101,
20291201,
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
CREATE PARTITION SCHEME [PS_CostDateByYearMonth]
AS PARTITION [PF_CostDateByYearMonth]
ALL TO ( [Partitionfg] );
CREATE UNIQUE CLUSTERED INDEX ptci ON Warm.DailyCosts(CalendarId,Id) ON [PS_CostDateByYearMonth](CalendarId) ;
USE [master]
GO
ALTER DATABASE [test] ADD FILE ( NAME = N'TestPartition', FILENAME = N'D:DATATestPartition.ndf' , SIZE = 3072KB , FILEGROWTH = 65536KB ) TO FILEGROUP [Partitionfg]
GO
USE [test]
GO
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20180101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2018
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20190101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2019
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20280101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2028
INSERT INTO Warm.AccountInfo(
Name ,
CorrelationId,
DayOfMonth
)
SELECT TOP(3000000) --3M
CAST(ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS varchar(10)) + 'a',
'00000000-0000-0000-0000-000000000000',
15
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
CREATE NONCLUSTERED INDEX [IX_Component_Value_DailyCosts] ON Warm.DailyCosts
(Component,Value)
ON [PS_CostDateByYearMonth](CalendarId);
GO
add a comment |
Reasoning
When comparing the column of the non partitioned table against the partitioned table, sql server will not be able to know what DayOfMonth
will hold, even if all of them are 15.
As a result it would not know which partitions to return to satisfy this filtering when joining the two tables.
A different example to give some more insight can be found here.
Testing
I was able to recreate your issue, for further questions please add as much information as possible.
This could be Table definition, indexes, partition scheme, ...
The DDL & DML is at the bottom.
When running the query we can get the same result:
62 partitions returned.
On the seek predicates it tries to filter what it can, this is the two filters on CalendarId
that do not have the + DayOfMonth-1
.
This translates to
WHERE ...
AND (( dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601))
...
When running the query with these filters you will see the same amount of rows returned on accessing the table in the query plan.
Only after getting this data, it can and will be joined to the AccountInfo
table on the ID
's and the DayOfMonth - 1
And to confirm when we change the query:
SELECT
ai.Name
,SUM(dc.Component )
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 - 1)))
GROUP BY Name
It does eleminate partitions:
Solution
I am going to assume that DayOfMonth
only goes up to 31
days.
If you know these boundaries, you can 'hard code' them so that sql server knows what partitions to look for. After this you can add the extra filtering.
E.G.
WITH CTE
AS
(
SELECT
ai.Name
,dc.Component
,dc.Value
,dc.CalendarId
,ai.DayOfMonth
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 )
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270701)))
)
SELECT Name,Component,SUM(Value)
FROM CTE
WHERE ((CalendarId >= (20190601 + DayOfMonth - 1)
AND CalendarId < 20190701)
OR (CalendarId >= 20270601
AND CalendarId < (20270601 + DayOfMonth - 1)))
GROUP BY
Name
,Component
The sole purpose of the cte
is to let sql server know that it can also filter on dc.CalendarId >= dc.CalendarId >= (20190601 ) OR dc.CalendarId < (20270701))
.
Sidenote: Adding a constraint does not work to enforce this.
This query gives us the result we want, with the correct partition elemination:
Only accessing partitions 19 & 21.
Test data
CREATE SCHEMA WARM
GO
CREATE TABLE Warm.DailyCosts(ID INT IDENTITY(1,1) NOT NULL,
Component int,
Value int,
CalendarId INT,
AccountInfoId int
)
CREATE TABLE Warm.AccountInfo(Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL,
Name VARCHAR(25),
CorrelationId uniqueidentifier,
DayOfMonth int
);
USE [master]
GO
ALTER DATABASE [test] ADD FILEGROUP [Partitionfg]
GO
USE [test]
GO
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
20190101,
20190201,
20190301,
20190401,
20190501,
20190601,
20190701,
20270601,
20270701,
20270801,
20270901,
20271001,
20271101,
20271201,
20280101,
20280201,
20280301,
20280401,
20280501,
20280601,
20280701,
20280801,
20280901,
20281001,
20281101,
20281201,
20290101,
20290201,
20290301,
20290401,
20290501,
20290601,
20290701,
20290801,
20290901,
20291001,
20291101,
20291201,
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
CREATE PARTITION SCHEME [PS_CostDateByYearMonth]
AS PARTITION [PF_CostDateByYearMonth]
ALL TO ( [Partitionfg] );
CREATE UNIQUE CLUSTERED INDEX ptci ON Warm.DailyCosts(CalendarId,Id) ON [PS_CostDateByYearMonth](CalendarId) ;
USE [master]
GO
ALTER DATABASE [test] ADD FILE ( NAME = N'TestPartition', FILENAME = N'D:DATATestPartition.ndf' , SIZE = 3072KB , FILEGROWTH = 65536KB ) TO FILEGROUP [Partitionfg]
GO
USE [test]
GO
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20180101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2018
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20190101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2019
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20280101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2028
INSERT INTO Warm.AccountInfo(
Name ,
CorrelationId,
DayOfMonth
)
SELECT TOP(3000000) --3M
CAST(ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS varchar(10)) + 'a',
'00000000-0000-0000-0000-000000000000',
15
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
CREATE NONCLUSTERED INDEX [IX_Component_Value_DailyCosts] ON Warm.DailyCosts
(Component,Value)
ON [PS_CostDateByYearMonth](CalendarId);
GO
add a comment |
Reasoning
When comparing the column of the non partitioned table against the partitioned table, sql server will not be able to know what DayOfMonth
will hold, even if all of them are 15.
As a result it would not know which partitions to return to satisfy this filtering when joining the two tables.
A different example to give some more insight can be found here.
Testing
I was able to recreate your issue, for further questions please add as much information as possible.
This could be Table definition, indexes, partition scheme, ...
The DDL & DML is at the bottom.
When running the query we can get the same result:
62 partitions returned.
On the seek predicates it tries to filter what it can, this is the two filters on CalendarId
that do not have the + DayOfMonth-1
.
This translates to
WHERE ...
AND (( dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601))
...
When running the query with these filters you will see the same amount of rows returned on accessing the table in the query plan.
Only after getting this data, it can and will be joined to the AccountInfo
table on the ID
's and the DayOfMonth - 1
And to confirm when we change the query:
SELECT
ai.Name
,SUM(dc.Component )
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 - 1)))
GROUP BY Name
It does eleminate partitions:
Solution
I am going to assume that DayOfMonth
only goes up to 31
days.
If you know these boundaries, you can 'hard code' them so that sql server knows what partitions to look for. After this you can add the extra filtering.
E.G.
WITH CTE
AS
(
SELECT
ai.Name
,dc.Component
,dc.Value
,dc.CalendarId
,ai.DayOfMonth
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 )
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270701)))
)
SELECT Name,Component,SUM(Value)
FROM CTE
WHERE ((CalendarId >= (20190601 + DayOfMonth - 1)
AND CalendarId < 20190701)
OR (CalendarId >= 20270601
AND CalendarId < (20270601 + DayOfMonth - 1)))
GROUP BY
Name
,Component
The sole purpose of the cte
is to let sql server know that it can also filter on dc.CalendarId >= dc.CalendarId >= (20190601 ) OR dc.CalendarId < (20270701))
.
Sidenote: Adding a constraint does not work to enforce this.
This query gives us the result we want, with the correct partition elemination:
Only accessing partitions 19 & 21.
Test data
CREATE SCHEMA WARM
GO
CREATE TABLE Warm.DailyCosts(ID INT IDENTITY(1,1) NOT NULL,
Component int,
Value int,
CalendarId INT,
AccountInfoId int
)
CREATE TABLE Warm.AccountInfo(Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL,
Name VARCHAR(25),
CorrelationId uniqueidentifier,
DayOfMonth int
);
USE [master]
GO
ALTER DATABASE [test] ADD FILEGROUP [Partitionfg]
GO
USE [test]
GO
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
20190101,
20190201,
20190301,
20190401,
20190501,
20190601,
20190701,
20270601,
20270701,
20270801,
20270901,
20271001,
20271101,
20271201,
20280101,
20280201,
20280301,
20280401,
20280501,
20280601,
20280701,
20280801,
20280901,
20281001,
20281101,
20281201,
20290101,
20290201,
20290301,
20290401,
20290501,
20290601,
20290701,
20290801,
20290901,
20291001,
20291101,
20291201,
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
CREATE PARTITION SCHEME [PS_CostDateByYearMonth]
AS PARTITION [PF_CostDateByYearMonth]
ALL TO ( [Partitionfg] );
CREATE UNIQUE CLUSTERED INDEX ptci ON Warm.DailyCosts(CalendarId,Id) ON [PS_CostDateByYearMonth](CalendarId) ;
USE [master]
GO
ALTER DATABASE [test] ADD FILE ( NAME = N'TestPartition', FILENAME = N'D:DATATestPartition.ndf' , SIZE = 3072KB , FILEGROWTH = 65536KB ) TO FILEGROUP [Partitionfg]
GO
USE [test]
GO
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20180101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2018
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20190101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2019
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20280101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2028
INSERT INTO Warm.AccountInfo(
Name ,
CorrelationId,
DayOfMonth
)
SELECT TOP(3000000) --3M
CAST(ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS varchar(10)) + 'a',
'00000000-0000-0000-0000-000000000000',
15
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
CREATE NONCLUSTERED INDEX [IX_Component_Value_DailyCosts] ON Warm.DailyCosts
(Component,Value)
ON [PS_CostDateByYearMonth](CalendarId);
GO
Reasoning
When comparing the column of the non partitioned table against the partitioned table, sql server will not be able to know what DayOfMonth
will hold, even if all of them are 15.
As a result it would not know which partitions to return to satisfy this filtering when joining the two tables.
A different example to give some more insight can be found here.
Testing
I was able to recreate your issue, for further questions please add as much information as possible.
This could be Table definition, indexes, partition scheme, ...
The DDL & DML is at the bottom.
When running the query we can get the same result:
62 partitions returned.
On the seek predicates it tries to filter what it can, this is the two filters on CalendarId
that do not have the + DayOfMonth-1
.
This translates to
WHERE ...
AND (( dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601))
...
When running the query with these filters you will see the same amount of rows returned on accessing the table in the query plan.
Only after getting this data, it can and will be joined to the AccountInfo
table on the ID
's and the DayOfMonth - 1
And to confirm when we change the query:
SELECT
ai.Name
,SUM(dc.Component )
,SUM(dc.Value) Value
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 - 1)
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270601 - 1)))
GROUP BY Name
It does eleminate partitions:
Solution
I am going to assume that DayOfMonth
only goes up to 31
days.
If you know these boundaries, you can 'hard code' them so that sql server knows what partitions to look for. After this you can add the extra filtering.
E.G.
WITH CTE
AS
(
SELECT
ai.Name
,dc.Component
,dc.Value
,dc.CalendarId
,ai.DayOfMonth
FROM
Warm.DailyCosts dc
JOIN Warm.AccountInfo ai
ON dc.AccountInfoId = ai.Id
WHERE
ai.CorrelationId = '00000000-0000-0000-0000-000000000000'
AND ((dc.CalendarId >= (20190601 )
AND dc.CalendarId < 20190701)
OR (dc.CalendarId >= 20270601
AND dc.CalendarId < (20270701)))
)
SELECT Name,Component,SUM(Value)
FROM CTE
WHERE ((CalendarId >= (20190601 + DayOfMonth - 1)
AND CalendarId < 20190701)
OR (CalendarId >= 20270601
AND CalendarId < (20270601 + DayOfMonth - 1)))
GROUP BY
Name
,Component
The sole purpose of the cte
is to let sql server know that it can also filter on dc.CalendarId >= dc.CalendarId >= (20190601 ) OR dc.CalendarId < (20270701))
.
Sidenote: Adding a constraint does not work to enforce this.
This query gives us the result we want, with the correct partition elemination:
Only accessing partitions 19 & 21.
Test data
CREATE SCHEMA WARM
GO
CREATE TABLE Warm.DailyCosts(ID INT IDENTITY(1,1) NOT NULL,
Component int,
Value int,
CalendarId INT,
AccountInfoId int
)
CREATE TABLE Warm.AccountInfo(Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL,
Name VARCHAR(25),
CorrelationId uniqueidentifier,
DayOfMonth int
);
USE [master]
GO
ALTER DATABASE [test] ADD FILEGROUP [Partitionfg]
GO
USE [test]
GO
CREATE PARTITION FUNCTION [PF_CostDateByYearMonth](int) AS RANGE RIGHT FOR VALUES (
20180101,
20180201,
20180301,
20180401,
20180501,
20180601,
20180701,
20180801,
20180901,
20181001,
20181101,
20181201,
20190101,
20190201,
20190301,
20190401,
20190501,
20190601,
20190701,
20270601,
20270701,
20270801,
20270901,
20271001,
20271101,
20271201,
20280101,
20280201,
20280301,
20280401,
20280501,
20280601,
20280701,
20280801,
20280901,
20281001,
20281101,
20281201,
20290101,
20290201,
20290301,
20290401,
20290501,
20290601,
20290701,
20290801,
20290901,
20291001,
20291101,
20291201,
20300101,
20300201,
20300301,
20300401,
20300501,
20300601,
20300701,
20300801,
20300901,
20301001,
20301101,
20301201)
CREATE PARTITION SCHEME [PS_CostDateByYearMonth]
AS PARTITION [PF_CostDateByYearMonth]
ALL TO ( [Partitionfg] );
CREATE UNIQUE CLUSTERED INDEX ptci ON Warm.DailyCosts(CalendarId,Id) ON [PS_CostDateByYearMonth](CalendarId) ;
USE [master]
GO
ALTER DATABASE [test] ADD FILE ( NAME = N'TestPartition', FILENAME = N'D:DATATestPartition.ndf' , SIZE = 3072KB , FILEGROWTH = 65536KB ) TO FILEGROUP [Partitionfg]
GO
USE [test]
GO
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20180101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2018
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20190101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2019
INSERT INTO Warm.DailyCosts(Component,
Value,
CalendarId,
AccountInfoId
)
SELECT TOP(1000000) --1M
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),
20280101 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 1000 % 1000 ,
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
-- 2028
INSERT INTO Warm.AccountInfo(
Name ,
CorrelationId,
DayOfMonth
)
SELECT TOP(3000000) --3M
CAST(ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS varchar(10)) + 'a',
'00000000-0000-0000-0000-000000000000',
15
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
CREATE NONCLUSTERED INDEX [IX_Component_Value_DailyCosts] ON Warm.DailyCosts
(Component,Value)
ON [PS_CostDateByYearMonth](CalendarId);
GO
edited 5 hours ago
answered 5 hours ago
Randi VertongenRandi Vertongen
7,8453 gold badges10 silver badges31 bronze badges
7,8453 gold badges10 silver badges31 bronze badges
add a comment |
add a comment |
Ben is a new contributor. Be nice, and check out our Code of Conduct.
Ben is a new contributor. Be nice, and check out our Code of Conduct.
Ben is a new contributor. Be nice, and check out our Code of Conduct.
Ben is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Database Administrators Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fdba.stackexchange.com%2fquestions%2f245575%2fsql-server-query-scanning-more-partitions-than-expected%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown