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)
(视图“聚合”同一个表,用于多个帐户)
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)
这是执行计划。
该视图有大约 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'
谓词的表上创建过滤索引(或统计信息)。作为一个侧面说明,如果该列是真/假,更好的数据类型选择,而不是varchar
会bit
。
计划的其余部分由嵌套循环左半连接驱动。对于来自散列连接的 36,222 行中的每一行,SQL Server:
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)
此查询的估计计划具有上述数据类型和索引更改:
归档时间: |
|
查看次数: |
20881 次 |
最近记录: |