检索日期范围的最有效方法


16

用这种表结构检索日期范围的最有效方法是什么?

create table SomeDateTable
(
    id int identity(1, 1) not null,
    StartDate datetime not null,
    EndDate datetime not null
)
go

假设您要同时为StartDate和设置一个范围EndDate。因此,换句话说,如果StartDate介于@StartDateBegin和之间@StartDateEnd,并且EndDate介于@EndDateBegin和之间@EndDateEnd,则执行某些操作。

我知道有几种方法可以解决此问题,但是最建议的是什么?

Answers:


29

通常,这是一个很难解决的问题,但是我们可以做一些事情来帮助优化器选择计划。该脚本创建一个具有10,000行的表,并具有行的已知伪随机分布,以说明:

CREATE TABLE dbo.SomeDateTable
(
    Id          INTEGER IDENTITY(1, 1) PRIMARY KEY NOT NULL,
    StartDate   DATETIME NOT NULL,
    EndDate     DATETIME NOT NULL
);
GO
SET STATISTICS XML OFF
SET NOCOUNT ON;
DECLARE
    @i  INTEGER = 1,
    @s  FLOAT = RAND(20120104),
    @e  FLOAT = RAND();

WHILE @i <= 10000
BEGIN
    INSERT dbo.SomeDateTable
        (
        StartDate, 
        EndDate
        )
    VALUES
        (
        DATEADD(DAY, @s * 365, {d '2009-01-01'}),
        DATEADD(DAY, @s * 365 + @e * 14, {d '2009-01-01'})
        )

    SELECT
        @s = RAND(),
        @e = RAND(),
        @i += 1
END

第一个问题是如何索引该表。一种选择是在DATETIME列上提供两个索引,因此优化器至少可以选择是在上搜索StartDate还是在上搜索EndDate

CREATE INDEX nc1 ON dbo.SomeDateTable (StartDate, EndDate)
CREATE INDEX nc2 ON dbo.SomeDateTable (EndDate, StartDate)

自然地,两者StartDate和的不等式EndDate意味着每个索引中只有一列可以支持示例查询中的查找,但这是我们所能做的最好的事情。我们可能考虑将每个索引中的第二列INCLUDE而不是键,但是我们可能还有其他查询,它们可以对前导列执行相等查找,而对第二列执行不相等查找。此外,我们可以通过这种方式获得更好的统计信息。无论如何...

DECLARE
    @StartDateBegin DATETIME = {d '2009-08-01'},
    @StartDateEnd DATETIME = {d '2009-10-15'},
    @EndDateBegin DATETIME = {d '2009-08-05'},
    @EndDateEnd DATETIME = {d '2009-10-22'}

SELECT
    COUNT_BIG(*)
FROM dbo.SomeDateTable AS sdt
WHERE
    sdt.StartDate BETWEEN @StartDateBegin AND @StartDateEnd
    AND sdt.EndDate BETWEEN @EndDateBegin AND @EndDateEnd

该查询使用变量,因此通常,优化器将猜测选择性和分布,从而得出81行的基数估计值。实际上,查询产生2076行,这一差异在更复杂的示例中可能很重要。

在SQL Server 2008 SP1 CU5或更高版本(或R2 RTM CU1)上,我们可以简单地通过添加到上面的查询中来利用参数嵌入优化来获得更好的估计。这将导致在批处理执行之前进行编译,从而使SQL Server可以“查看”实际参数值并对其进行优化。进行此更改后,估算值将提高到468行(尽管您确实需要检查运行时计划才能看到此行)。此估算值比81行要好,但仍不能完全关闭。在某些情况下,由跟踪标志2301启用的建模扩展可能会有所帮助,但此查询不能。OPTION (RECOMPILE)SELECT

问题是两个范围搜索限定的行重叠。优化程序的成本核算和基数估计组件中进行的简化假设之一是谓词是独立的(因此,如果两个谓词的选择性均为50%,则应用这两个谓词的结果将限定50%的行等于50%的行) )。在这种相关性存在问题的地方,我们通常可以使用多列和/或过滤后的统计数据来解决它。在两个范围的起点和终点未知的情况下,这变得不切实际。这是我们有时不得不诉诸于将查询重写为恰好产生更好估计的形式的地方:

SELECT COUNT(*) FROM
(
    SELECT
        sdt.Id
    FROM dbo.SomeDateTable AS sdt
    WHERE 
        sdt.StartDate BETWEEN @StartDateBegin AND @StartDateEnd
    INTERSECT
    SELECT
        sdt.Id
    FROM dbo.SomeDateTable AS sdt 
    WHERE
        sdt.EndDate BETWEEN @EndDateBegin AND @EndDateEnd
) AS intersected (id)
OPTION (RECOMPILE)

这种形式碰巧会产生2110行的运行时估计(相对于实际的2076行)。除非您使用TF 2301,否则这种情况下,更高级的建模技术将使您理解并获得与以前完全相同的估计:468行。

有一天,SQL Server可能会获得对间隔的本地支持。如果这提供了良好的统计支持,则开发人员可能会像这样少调整一下查询计划。


5

我不知道一种适用于所有数据分发的快速解决方案,但是如果您的所有范围都很短,我们通常可以加快速度。例如,如果范围短于一天,则代替此查询:

SELECT  TaskId ,    
        TaskDescription ,
        StartedAt ,    
        FinishedAt    
FROM    dbo.Tasks    
WHERE   '20101203' BETWEEN StartedAt AND FinishedAt

我们可以再添加一个条件:

SELECT  TaskId ,    
        TaskDescription ,
        StartedAt ,    
        FinishedAt    
FROM    dbo.Tasks    
WHERE   '20101203' BETWEEN StartedAt AND FinishedAt
    AND StartedAt >= '20101202'
    AND FinishedAt <= '20101204' ;

结果,查询将只扫描两天的范围,而不是扫描整个表,这更快。如果范围可能更长,我们可以将它们存储为较短范围的序列。此处的详细信息:在约束的帮助下调整SQL查询

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.