简单的视图查询需要很长时间

wor*_*ufz 2 performance sql-server view query-performance

Windows Server 2012、Microsoft SQL Server。

我有一个存储过程(见下文),它创建了一个我需要查询的视图。存储过程部分运行良好,大约需要 5 秒钟才能完成,然后创建了视图。

该视图有大约 30-35k 行。

我的问题是对创建的视图运行一个简单的查询需要大约 20 分钟!一个简单的查询,例如:

SELECT COUNT(*) FROM MY_VIEW
Run Code Online (Sandbox Code Playgroud)

上面的查询大约需要 20 分钟才能完成,直到它返回行数。对实际表(视图包含)运行相同的查询立即返回结果!

我不确定存储过程是否相关,因为视图是立即创建的并且查询它们是我遇到的问题,但我发布它以防万一。

我想提一下,由相同存储过程创建的其他视图,包含少量行(数百行)对查询的响应速度相当快......所以行数肯定是这里的一个因素。

我不明白的是为什么查询一个 30k 行的表会在 2 秒内返回结果,而对 30k 行视图执行相同的查询需要 20 分钟。

存储过程

USE [QUARTERLY_SEC_REPORT]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[DynamicView_QR_VisitsDistSummary] 
AS
BEGIN
DECLARE @CurrentView nvarchar(MAX) = null
DECLARE @SchemaName nvarchar(400)
DECLARE @TableName nvarchar(400)
DECLARE @DynSQL nvarchar(MAX)
DECLARE @DateModifier nvarchar(400)
DECLARE @DynDROP nvarchar(MAX) = 'DROP VIEW Unified_QR_VisitsDistSummary'
DECLARE @InclusionTable nvarchar(MAX) = '[dbo].[QUARTERLY_VIEW]'

Set @DynSQL = 'CREATE VIEW Unified_QR_VisitsDistSummary AS '

set @CurrentView = (select VIEW_DEFINITION from INFORMATION_SCHEMA.VIEWS WHERE TABLE_SCHEMA='dbo' and TABLE_NAME='Unified_QR_VisitsDistSummary')

DECLARE cursor1 CURSOR FOR 
    select TABLE_SCHEMA,TABLE_NAME
    from INFORMATION_SCHEMA.TABLES
    where 
    TABLE_SCHEMA='dbo' AND
    TABLE_NAME like 'visits_dist_summary_ACC_%'

OPEN cursor1

FETCH NEXT FROM cursor1 INTO @SchemaName, @TableName

WHILE @@FETCH_STATUS = 0
BEGIN
    -- Add the select code.
    Set @DateModifier = '( SELECT MAX([retrieved_at]) FROM '+ @SchemaName +'.' + @TableName + ')'
    Set @DynSQL = @DynSQL + 'Select * from ' + @SchemaName +'.' + @TableName +' INNER JOIN '+ @InclusionTable+ ' ON '+ @InclusionTable +'.AccountID  = ' + @SchemaName +'.' + @TableName+ '.Account_ID WHERE ' + @InclusionTable +'.Appear_In_View =''True'' AND (retrieved_at =' + @DateModifier +' OR retrieved_at = DATEADD (MINUTE, -1, '+@DateModifier+ ')'+' OR retrieved_at = DATEADD (MINUTE, -2, '+@DateModifier+ ')' +' OR retrieved_at = DATEADD (MINUTE, -3, '+@DateModifier+ '))'
    FETCH NEXT FROM cursor1
    INTO @SchemaName, @TableName

    -- If the loop continues, add the UNION ALL statement.
    If @@FETCH_STATUS = 0
    BEGIN
        Set @DynSQL = @DynSQL + ' UNION ALL '
    END

END
IF @CurrentView = @DynSQL 
    PRINT 'VIEW IS THE SAME, NEW VIEW WASN''T CREATED'
ELSE
    BEGIN
        if @CurrentView is not null
        BEGIN
        print @DynDROP
            exec sp_executesql @DynDROP
        END
        PRINT @DynSQL
        exec sp_executesql @DynSQL
    END
END
Run Code Online (Sandbox Code Playgroud)

查看定义

SELECT        *
FROM            dbo.visits_dist_summary_ACC_12345 INNER JOIN
                         [dbo].[QUARTERLY_VIEW] ON [dbo].[QUARTERLY_VIEW].AccountID = dbo.visits_dist_summary_ACC_12345.Account_ID
WHERE        [dbo].[QUARTERLY_VIEW].Appear_In_View = 'True' AND (retrieved_at =
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_12345) OR
                         retrieved_at = DATEADD(MINUTE, - 1,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_12345)) OR
                         retrieved_at = DATEADD(MINUTE, - 2,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_12345)) OR
                         retrieved_at = DATEADD(MINUTE, - 3,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_12345)))
UNION ALL
SELECT        *
FROM            dbo.visits_dist_summary_ACC_22222 INNER JOIN
                         [dbo].[QUARTERLY_VIEW] ON [dbo].[QUARTERLY_VIEW].AccountID = dbo.visits_dist_summary_ACC_22222.Account_ID
WHERE        [dbo].[QUARTERLY_VIEW].Appear_In_View = 'True' AND (retrieved_at =
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_22222) OR
                         retrieved_at = DATEADD(MINUTE, - 1,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_22222)) OR
                         retrieved_at = DATEADD(MINUTE, - 2,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_22222)) OR
                         retrieved_at = DATEADD(MINUTE, - 3,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_22222)))
UNION ALL
SELECT        *
FROM            dbo.visits_dist_summary_ACC_77777 INNER JOIN
                         [dbo].[QUARTERLY_VIEW] ON [dbo].[QUARTERLY_VIEW].AccountID = dbo.visits_dist_summary_ACC_77777.Account_ID
WHERE        [dbo].[QUARTERLY_VIEW].Appear_In_View = 'True' AND (retrieved_at =
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_77777) OR
                         retrieved_at = DATEADD(MINUTE, - 1,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_77777)) OR
                         retrieved_at = DATEADD(MINUTE, - 2,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_77777)) OR
                         retrieved_at = DATEADD(MINUTE, - 3,
                             (SELECT        MAX([retrieved_at])
                               FROM            dbo.visits_dist_summary_ACC_77777)))
Run Code Online (Sandbox Code Playgroud)

表结构(sp_help 输出)

(视图“聚合”同一个表,用于多个帐户)

Name
visits_dist_summary_ACC_12345

account_id,varchar,no,12,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
siteid,varchar,no,12,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
countryCode,varchar,no,50,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
countryCount,float,no,8,53   ,NULL,yes,(n/a),(n/a),NULL
agentCode,varchar,no,100,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
agentCount,float,no,8,53   ,NULL,yes,(n/a),(n/a),NULL
retrieved_at,varchar,no,100,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
relevant_month,varchar,no,100,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS
domain_name,varchar,no,100,     ,     ,yes,no,yes,SQL_Latin1_General_CP1_CI_AS

Identity    Seed    Increment   Not For Replication
No identity column defined. NULL    NULL    NULL

RowGuidCol
No rowguidcol column defined.

Data_located_on_filegroup
PRIMARY
Run Code Online (Sandbox Code Playgroud)

这是执行计划

Pau*_*ite 5

该视图有大约 30-35k 行。

视图(没有聚集索引)只是一个存储的查询定义。它不直接包含任何行。

我的问题是对创建的视图运行一个简单的查询需要大约 20 分钟

这需要执行存储的查询定义。基表(和视图查询)存在一些数据类型问题并且缺乏有用的索引,这导致每次访问视图时都要执行大量的工作(解释如下)。

数据类型和正确性问题

该列retrieved_at当前的类型为varchar(100)。您应该改用正确的日期/时间类型。除了性能方面的考虑,您现在几乎肯定会得到不正确的结果

MAX(retrieved_at)将找到排序最高的字符串,而不是最近的值datetime。涉及 a 的比较DATEADD最终转换为 a datetime,但仅在MAX找到 之后(作为字符串)。

理想情况下,您将转换基表以便使用正确的数据类型,例如使用:

ALTER TABLE dbo.visits_dist_summary_ACC_12345
ALTER COLUMN retrieved_at datetime NOT NULL;
Run Code Online (Sandbox Code Playgroud)

总体战略

从问题中不清楚,但您可能打算不时存储数据的快照。如果是这种情况,将动态查询的结果写入永久表会比视图高效得多。

当前执行计划

您提供的计划强调了几个问题。鉴于已经提到的要点,其中一些可能无关紧要,因此提供这些内容是为了兴趣。

计划

帐户 ID 上的哈希联接产生 36,222 行,而预期只有 1 行。这表明该连接中涉及的一个或两个表的统计信息已过期。

更新统计信息可能会改进该估计,但您可能需要更进一步,例如在[QUARTERLY_VIEW]带有Appear_In_View = 'true'谓词的表上创建过滤索引(或统计信息)。作为一个侧面说明,如果该列是真/假,更好的数据类型选择,而不是varcharbit

计划的其余部分由嵌套循环左半连接驱动。对于来自散列连接的 36,222 行中的每一行,SQL Server:

  • 从基表中读取每一行(表扫描)
  • 找到retred_at` MAX()(流聚合)
  • 测试结果是否与外部retrieved_at值匹配。

请注意,对于初始散列连接产生的 36,222 行中的每一行,都会发生此过程(完整扫描、聚合、过滤)。

更糟糕的是,如果第一个 scan-aggregate-filter 分支没有找到匹配项(满足半连接),SQL Server 继续完全运行相同的进程,对于 -1、-2 和 -3 分钟的情况。

上面执行计划中显示的数字(使用 SQL Sentry Plan Explorer)表示每个运算符在嵌套循环连接的所有迭代中产生的总行数。在 SSMS 中,您需要查看每个运算符的实际行数属性。

对于最上面的 scan-aggregate-filter 分支,这个总数是1,312,033,284 行。第二个分支贡献了额外的 436,185,324 行。如果需要 -2 和 -3 分钟的案例来找到匹配的行,情况会更糟。希望您能明白为什么“简单查询”会运行 20 分钟。

行动

  • 更正数据类型
  • 更新统计
  • 如果满足您的需要,请使用表来存储静态快照
  • retrieved_at例如上创建索引

    CREATE NONCLUSTERED INDEX i2 
    ON dbo.visits_dist_summary_ACC_12345 (retrieved_at);
    
    Run Code Online (Sandbox Code Playgroud)
  • 评估一个聚集索引,account_id例如

    CREATE CLUSTERED INDEX i1 
    ON dbo.visits_dist_summary_ACC_12345 (account_id);
    
    Run Code Online (Sandbox Code Playgroud)

上述步骤应该会非常显着地提高性能,尤其是索引retrieved_at(键入为日期时间)。

MAX由于优化器的限制/优先级,可能需要重写查询以避免计算四次,但索引应该使该操作变得微不足道(从索引的末尾读取一行),因此实际上不需要。


如果它有用,一种查询重写方法是:

SELECT
    QV.AccountID,
    QV.Appear_In_View,
    QV.Report_Name,
    VDSA.account_id,
    VDSA.siteid,
    VDSA.countryCode,
    VDSA.countryCount,
    VDSA.agentCode,
    VDSA.agentCount,
    VDSA.retrieved_at,
    VDSA.relevant_month,
    VDSA.domain_name
FROM dbo.QUARTERLY_VIEW AS QV
JOIN  dbo.visits_dist_summary_ACC_12345 AS VDSA
    ON VDSA.account_id = QV.AccountID
WHERE
    QV.Appear_In_View = 'True' 
    AND VDSA.retrieved_at IN
    (
        SELECT 
            V.max_date_candidates
        FROM 
        (
            -- Compute maximum date once
            SELECT TOP (1)
                VDSA2.retrieved_at
            FROM dbo.visits_dist_summary_ACC_12345 AS VDSA2
            ORDER BY
                VDSA2.retrieved_at DESC
        ) AS Q (max_retrieved_at)
        CROSS APPLY
        (
            -- Generate four rows based on the maximum date
            VALUES 
                (DATEADD(MINUTE, -0, Q.max_retrieved_at)),
                (DATEADD(MINUTE, -1, Q.max_retrieved_at)),
                (DATEADD(MINUTE, -2, Q.max_retrieved_at)),
                (DATEADD(MINUTE, -3, Q.max_retrieved_at))
        ) AS V (max_date_candidates)
    );
Run Code Online (Sandbox Code Playgroud)

此查询的估计计划具有上述数据类型和索引更改:

改进计划