视图上的聚合操作将忽略索引


8

场景

曾几何时,一家小公司中有一个Staging数据库,该数据库正在参与ETL流程,充当来自多个第三方来源的各种格式文件的接收目录。E是通过DTS程序包处理的,几乎没有用于审计或控制的控制结构,但是被认为“足够好”,并且从所有意图和目的来看都是如此。

E部分提供的数据旨在供单个应用程序使用,并由少数年轻有能力的程序员开发和管理。尽管他们缺乏当时的数据仓库技术经验或知识,但他们还是根据应用程序代码创建并创建了自己的T和L流程。这些崭露头角的软件工程师大胆创新,发明了局外人所谓的“低于理想的轮子”,但是凭借“足够好”作为永远存在的服务水平,他们能够提供一个操作框架。

一段时间以来,在紧密耦合的领域中一切都很好,暂存目录以12个第三方的数据为美食,反过来又由应用程序提供。随着应用程序的增长,它的胃口也随之增加,但是随着熟练的白骑士开发人员对系统的观察,这些胃口很快得到了解决,在很多情况下甚至都得到了很好的解决。

但是,黄金时代不可能永远持续下去。随着成功应用程序的繁荣,业务不断发展。随着它的增长,登台环境和应用程序也随之增长。尽管他们保持了警惕,但只有少数英雄开发人员无法跟上现在扩展的系统的维护,并且消费者已经有权使用其数据。不再是他们需要甚至想要的事了,但是民众感到他们只是应得的,甚至要求更多。

有了很多钱,武装起来的公司就进入了市场,雇用了开发人员和管理员来帮助支持不断发展的系统。各式各样的雇佣兵蜂拥而至,但是随着这种增长突飞猛进,可用的专家指导却很少。新的开发人员和管理人员都在努力理解自制套件的复杂性,直到挫折导致全面的战争。每个部门都开始尝试单独解决每个问题,相互之间的工作比相互之间的工作更多。一个项目或计划将以几种不同的方式实施,每种方式与下一个略有不同。对于某些白骑士来说,这一切的压力实在太大了,当他们沦陷时,帝国崩溃了。不久,系统陷入混乱,

尽管这些承诺可以转换成意大利面条代码的领域已经发生了变化,但该公司还是坚持了下来。毕竟是“足够好”。

挑战

后来发生了一些政权更迭和招聘热潮,我发现自己从事公司的雇用。自大战以来已经有很多年了,但是所造成的损失仍然非常明显。我设法解决了系统E部分中的一些弱点,并以将DTS软件包升级到SSIS为幌子添加了一些控制表,现在一些实际的数据仓库专业人员正在使用它们创建正常的并记录了T和L替换。

第一个障碍是从第三方文件中导入数据,这种方式不会截断值或更改本机数据类型,但还包括一些用于重新加载和清除的控制键。这一切都很好,但是应用程序需要能够以无缝,透明的方式访问这些新表。DTS包可以填充一个表,然后由应用程序直接读取该表。由于QA的原因,SSIS升级需要并行进行,但是这些新软件包包括各种控制键,并且还利用了分区方案,更不用说实际的元数据更改足以显着保证一个新表的总和,因此新表用于新的SSIS包。

随着可靠的数据导入现已开始运作,并且由仓库团队使用,真正的挑战在于将新数据提供给直接访问暂存环境的应用程序,而对应用程序代码的影响最小(也称为“否”)。为此,我选择使用视图,重命名表(例如dbo.DailyTransactionto)dbo.DailyTranscation_LEGACYdbo.DailyTransaction为视图重用对象名称,这实际上只是从现在开始选择所有内容。LEGACY指定表。由于从业务的角度来看,重新加载这些表中包含的数据的年数是不可行的,因为新的SSIS填充和分区表已进入生产阶段,因此关闭了旧的DTS导入,并且应用程序必须能够以及访问新表中的新数据。此时,将更新视图以在dbo.DailyTransactionComplete可用时从新表(例如)中选择数据,而在不可用时从旧表中选择数据。

实际上,正在执行以下操作:

CREATE VIEW dbo.DailyTransaction
AS  SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.DailyTransactionComplete
    UNION ALL
    SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.DailyTransaction_LEGACY l
    WHERE NOT EXISTS (  SELECT  1
                        FROM    dbo.DailyTransactionComplete t
                        WHERE   t.FileDate = l.FileDate );

虽然从逻辑上讲是合理的,但在许多聚合情况下根本无法很好地执行,通常会导致执行计划对遗留表中的数据执行完整索引扫描。对于几十亿条记录来说这可能很好,但对于几十亿条记录则不是那么好。由于实际上是后者,因此我不得不求助于...“创意”,从而引导我创建索引视图。

下面是一些测试情况下,我已经设置了包括FileDate控制键已经被移植到了数据仓库兼容DateCode_FK端口,用于说明如何彻底一点我关心对新表的查询是优化搜索暂且:

USE tempdb;
GO

SET NOCOUNT ON;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'DailyTransaction_LEGACY'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.DailyTransaction_LEGACY;
    CREATE TABLE dbo.DailyTransaction_LEGACY
    (
        DailyTransaction_PK         BIGINT IDENTITY( 1, 1 ) NOT NULL,
        FileDate                    DATETIME NOT NULL,
        Foo                         INT NOT NULL
    );

    INSERT INTO dbo.DailyTransaction_LEGACY ( FileDate, Foo )
    SELECT  DATEADD( DAY, ( 1 - ROW_NUMBER() 
                OVER( ORDER BY so1.object_id ) - 800 ) % 1000, 
                CONVERT( DATE, GETDATE() ) ),
            so1.object_id % 1000 + so2.object_id % 1000
    FROM    sys.all_objects so1
    CROSS JOIN sys.all_objects so2;

    ALTER TABLE dbo.DailyTransaction_LEGACY
    ADD CONSTRAINT PK__DailyTrainsaction
        PRIMARY KEY CLUSTERED ( DailyTransaction_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'DailyTransactionComplete'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.DailyTransactionComplete;
    CREATE TABLE dbo.DailyTransactionComplete
    (
        DailyTransaction_PK            BIGINT IDENTITY( 1, 1 ) NOT NULL,
        DateCode_FK                    INTEGER NOT NULL,
        Foo                            INTEGER NOT NULL
    );

    INSERT INTO dbo.DailyTransactionComplete ( DateCode_FK, Foo )
    SELECT  TOP 100000
            CONVERT( INTEGER, CONVERT( VARCHAR( 8 ), DATEADD( DAY, 
                ( 1 - ROW_NUMBER() OVER( ORDER BY so1.object_id ) ) % 100, 
                GETDATE() ), 112 ) ),
            so1.object_id % 1000
    FROM    sys.all_objects so1
    CROSS JOIN sys.all_objects so2;

    ALTER TABLE dbo.DailyTransactionComplete
    ADD CONSTRAINT PK__DailyTransaction
        PRIMARY KEY CLUSTERED ( DateCode_FK, DailyTransaction_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );        
END;
GO

在我的本地沙箱上,以上内容使我得到了一个具有约440万行的旧表和一个包含10万行的新表,其中DateCode_FK/ FileDate值有些重叠。

MAX( FileDate )对没有额外的指标遗留表什么我所期望的运行。

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput        DATETIME;
SELECT  @ConsumeOutput = MAX( FileDate )
FROM    dbo.DailyTransaction_LEGACY;

SET STATISTICS IO, TIME OFF;
GO

表“ DailyTransaction_LEGACY”。扫描计数1,逻辑读9228,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 889毫秒,经过的时间= 886毫秒。

聚簇索引,旧版

在表上扔一个简单的索引会使事情变得更好。仍然是扫描,但是扫描的是一条记录,而不是440万条记录。我很酷。

CREATE NONCLUSTERED INDEX IX__DailyTransaction__FileDate
    ON    dbo.DailyTransaction_LEGACY ( FileDate );

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput        DATETIME;
SELECT  @ConsumeOutput = MAX( FileDate )
FROM    dbo.DailyTransaction_LEGACY;

SET STATISTICS IO, TIME OFF;
GO

SQL Server解析和编译时间:CPU时间= 0毫秒,经过的时间= 1毫秒。表“ DailyTransaction_LEGACY”。扫描计数1,逻辑读3,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 0毫秒,经过的时间= 0毫秒。

非聚集索引,旧版

现在,创建视图,以便开发人员不必更改任何代码,因为按照我们所知,那显然将是世界的尽头。各种灾难。

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'DailyTransaction'
                    AND type = 'V' )
BEGIN
    EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO

ALTER VIEW dbo.DailyTransaction
AS  SELECT  DailyTransaction_PK, FileDate = CONVERT( 
                DATETIME, CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ), Foo
    FROM    dbo.DailyTransactionComplete
    UNION ALL    
    SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.DailyTransaction_LEGACY l
    WHERE   NOT EXISTS (    SELECT  1
                            FROM    dbo.DailyTransactionComplete t
                            WHERE   CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
                                        t.DateCode_FK ), 112 ) = l.FileDate );
GO

是的,子查询很糟糕,但这不是问题,我可能会简单地创建一个持久化的计算列,并在解决实际问题时为此目的在其上添加索引。因此,事不宜迟,

问题

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput1        DATETIME;
SELECT  @ConsumeOutput1 = MAX( FileDate )
FROM    dbo.DailyTransaction;

SET STATISTICS IO, TIME OFF;
GO

SQL Server解析和编译时间:CPU时间= 0毫秒,经过的时间= 4毫秒。表“ DailyTransaction_LEGACY”。扫描计数1,逻辑读11972,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'Worktable'。扫描计数0,逻辑读0,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'Workfile'。扫描计数0,逻辑读0,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'DailyTransactionComplete'。扫描计数2,逻辑读620,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 983毫秒,经过的时间= 983毫秒。

查看计划

哦,我知道了,Sql Server试图告诉我我在做什么是愚蠢的。虽然我基本上同意,但这并不能改变我的困境。对于查询中包含在谓词中FileDatedbo.DailyTransaction视图的查询,这实际上非常有用,但是,尽管该MAX计划很糟糕,但该TOP计划将整个问题向南发送。真正的南方。

SET STATISTICS IO, TIME ON;

SELECT  TOP 10 FileDate
FROM    dbo.DailyTransaction
GROUP BY FileDate 
ORDER BY FileDate DESC

SET STATISTICS IO, TIME OFF;
GO

表“ DailyTransactionComplete”。扫描计数2,逻辑读1800110,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'DailyTransaction_LEGACY'。扫描计数1,逻辑读1254,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'Worktable'。扫描计数0,逻辑读0,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'Workfile'。扫描计数0,逻辑读0,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 109559毫秒,经过的时间= 109664毫秒。

最佳

我提到过较早获得“创意”,这可能会产生误导。我的意思是说“更愚蠢”,因此我试图在聚合操作期间使该视图起作用的尝试是在dbo.DailyTransactionCompletedbo.DailyTransaction_LEGACY表上创建视图,模式绑定并索引后者,然后在带有NOEXPAND提示的另一个视图中使用这些视图在旧版视图上。尽管它或多或少地适合目前需要做的工作,但我发现整个“解决方案”都令人不安,最终达到以下目的:

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'v_DailyTransactionComplete'
                    AND type = 'V' )
BEGIN
    EXEC( 'CREATE VIEW dbo.v_DailyTransactionComplete AS SELECT x = 1;' );
END;
GO

ALTER VIEW dbo.v_DailyTransactionComplete
AS  SELECT  DailyTransaction_PK, FileDate = CONVERT( DATETIME, 
                CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ), 
            Foo
    FROM    dbo.DailyTransactionComplete;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'v_DailyTransaction_LEGACY'
                    AND type = 'V' )
BEGIN
    EXEC( 'CREATE VIEW dbo.v_DailyTransaction_LEGACY AS SELECT x = 1;' );
END;
GO

ALTER VIEW dbo.v_DailyTransaction_LEGACY
WITH SCHEMABINDING
AS  SELECT  l.DailyTransaction_PK,
            l.FileDate,
            l.Foo,
            CountBig = COUNT_BIG( * )
    FROM    dbo.DailyTransaction_LEGACY l
    INNER JOIN dbo.DailyTransactionComplete n
        ON  l.FileDate <> CONVERT( DATETIME, CONVERT( VARCHAR( 8 ), 
                n.DateCode_FK ), 112 )
    GROUP BY l.DailyTransaction_PK,
            l.FileDate,
            l.Foo;
GO

CREATE UNIQUE CLUSTERED INDEX CI__v_DailyTransaction_LEGACY
    ON dbo.v_DailyTransaction_LEGACY ( FileDate, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 80 );
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'DailyTransaction'
                    AND type = 'V' )
BEGIN
    EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO

ALTER VIEW dbo.DailyTransaction
AS  SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.v_DailyTransactionComplete
    UNION ALL    
    SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.v_DailyTransaction_LEGACY WITH ( NOEXPAND );
GO

强迫优化器使用索引视图提供的索引可以使MAXTOP问题消失,但是必须有一种更好的方法来实现我在这里试图做的事情。绝对任何建议/责骂将不胜感激!

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput1        DATETIME;
SELECT  @ConsumeOutput1 = MAX( FileDate )
FROM    dbo.DailyTransaction;

SET STATISTICS IO, TIME OFF;
GO

表'v_DailyTransaction_LEGACY'。扫描计数1,逻辑读3,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'DailyTransactionComplete'。扫描计数1,逻辑读310,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 31毫秒,经过的时间= 36毫秒。

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput1        DATETIME;
SELECT  TOP 10 @ConsumeOutput1 = FileDate
FROM    dbo.DailyTransaction
GROUP BY FileDate 
ORDER BY FileDate DESC

SET STATISTICS IO, TIME OFF;
GO

表'v_DailyTransaction_LEGACY'。扫描计数1,逻辑读101,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'Worktable'。扫描计数0,逻辑读0,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'Workfile'。扫描计数0,逻辑读0,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。表'DailyTransactionComplete'。扫描计数1,逻辑读310,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 63毫秒,经过的时间= 66毫秒。

TL; DR:

帮助我了解在我提到的第一个视图上进行聚合查询所需的操作,该视图在合理的时间内运行且具有合理的I / O资源利用率。


3
非索引视图不存储任何数据,并且您不能使用子查询,联合等对视图进行索引。我认为您需要考虑以不同的方式实现数据,例如将视图拆分为两个视图然后针对它们进行查询,或者完全绕过该视图。
亚伦·伯特兰

我想我正在努力实现您的建议,但是我没有完全掌握我需要做的事情。我认为,我已经成功地将创可贴作为索引进行了编排和实现的遗留视图似乎是解决子查询限制的一个不错的解决方法,尽管它在当前状态下或多或少都可以工作,但很容易受到其他问题的影响。范围蠕变。我正在考虑建立一个过程,以便在导入后填充一个全新的基本表,并更改视图以引用该视图。
Avarkx 2014年

Answers:


4

改写NOT EXISTSDISTINCT在不等式JOIN所允许索引的观点,但也有很好的理由,这是并不常见。

为在视图上建立索引而生成的执行计划是不可避免的。不等式强制执行嵌套循环物理连接,该连接除一个值外为交叉连接。假设join列不可为空(如示例代码中所示),那么将产品与不同或等效的group by折叠将产生正确的结果,但是它永远不会有效。随着时间的流逝,涉及的表格越来越大,这种低效率只会变得越来越糟。

类似的问题会影响任何影响该视图引用的表的DML语句的执行计划(因为在SQL Server中,该视图必须始终同步到基表)。查看为在任一表中添加或修改单行而生成的执行计划,以了解我的意思。

总体而言,您要解决的问题是SQL Server查询优化器并不总是针对包含的视图生成良好的计划UNION ALL。我们认为理所当然的许多优化(例如MAX-> TOP (1))都无法在所有并集上实现。

对于您解决的每个问题,您都会发现另一种情况,即正常和预期的优化均未发生,从而导致执行计划的性能令人失望。显而易见的解决方案是避免在视图中使用联合。在您的情况下,如何实施此操作取决于细节,尽管问题中有细节,但可能只有您知道。

如果有足够的空间,一种解决方案是分别维护completelegacy基础表(包括不存在的逻辑)。这的确会导致数据重复,并带来同步问题,但是以我的经验,与在所有(甚至大多数)情况下尝试获取联合视图以生成适用于各种查询的良好执行计划相比,解决这些问题要容易得多。

我确信您知道,SQL Server提供了许多辅助数据同步的功能,包括更改跟踪,更改数据捕获,触发器等。实现的细节超出了该论坛的范围。重要的一点是为优化器提供基本表,而不是合并所有视图。


感谢您和@AaronBertrand的输入。非常感谢您的见解和建议!我可能最终将手动将旧表数据迁移到新表中,以便可以更改视图,以完全不再需要与旧表进行合并。从理论上讲,我应该可以完全删除旧表。这种方法还会遇到其他挑战,但是正如您已经提到的那样,从长远来看,也许这些挑战将更易于管理,因为我正在做的事情显然永远无法奏效。
Avarkx 2014年
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.