Windows Server 2012、Microsoft SQL Server。
クエリする必要のあるビューを作成するストアドプロシージャ(下記参照)があります。ストアドプロシージャパーツは適切に機能し、終了まで5秒ほどかかり、ビューが作成されます。
ビューには約30〜35k行があります。
私の問題は、作成されたビューに対して単純なクエリを実行すると、約20分かかることです。次のような簡単なクエリ:
SELECT COUNT(*) FROM MY_VIEW
上記のクエリは、行数を返すまで約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
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)))
(ビューは、複数のアカウントに対して同じテーブルを「集約」します)
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
これが 実行計画 です。
ビューには約30〜35k行があります。
ビュー(クラスター化インデックスなし)は、単に格納されたクエリ定義です。行は直接含まれていません。
私の問題は、作成されたビューに対して単純なクエリを実行すると約20分かかることです
これには、保存されたクエリ定義を実行する必要があります。ベーステーブル(およびビュークエリ)には、いくつかのデータ型の問題と有用なインデックスの不足があり、ビューにアクセスするたびに異常な量の作業が実行されます(以下で説明します)。
列_retrieved_at
_は現在varchar(100)
として型指定されています。代わりに適切な日付/時刻型を使用する必要があります。パフォーマンスの考慮事項は別として、現在、ほぼ確実に正しくない結果が得られています。
MAX(retrieved_at)
は、datetime
条件の最新の値ではなく、最高にソートされるstringを検索します。 DATEADD
を含む比較は、最終的にdatetime
に変換されますが、MAX
が(文字列として)見つかった後でのみです。
理想的には、正しいデータ型が使用されるようにベーステーブルを変換します。
_ALTER TABLE dbo.visits_dist_summary_ACC_12345
ALTER COLUMN retrieved_at datetime NOT NULL;
_
質問からは明らかではありませんが、時々データのスナップショットを保存しようとしている可能性があります。その場合は、ビューよりも動的クエリの結果を永続テーブルに書き込む方がはるかに効率的です。
あなたが提供した計画はいくつかの問題を強調しています。すでに言及されている点を考慮すると、これの一部は無関係である可能性があるため、これは関心のために提供されています。
アカウントIDのハッシュ結合は36,222行を生成しますが、1行しか期待されていませんでした。これは、その結合に関係するテーブルの一方または両方の統計が古くなっていることを示しています。
統計を更新すると、その見積もりが改善される可能性がありますが、さらに進める必要がある場合もあります。 _[QUARTERLY_VIEW]
_述語を使用して、_Appear_In_View = 'true'
_テーブルにフィルター選択されたインデックス(または統計)を作成します。補足として、その列がtrue/falseの場合、varchar
よりも優れたデータ型の選択はbit
になります。
残りの計画は、ネストされたループの左準結合によって駆動されます。ハッシュ結合からの36,222行のそれぞれについて、SQL Serverは次のことを行います。
MAX()
retrieved_at`(Stream Aggregate)を検索しますretrieved_at
_値と一致するかどうかをテストして確認します。このプロセス(フルスキャン、集計、フィルター)は、最初のハッシュ結合によって生成された36,222行のすべての行に対して発生することに注意してください。
さらに悪いことに、最初のscan-aggregate-filterブランチで一致が見つからない場合(準結合を満たす)、SQL Serverは、-1、-2、および-3分の場合に、同じプロセスを完全に再度実行し続けます。
上記の実行計画(SQL Sentry Plan Explorerを使用)に示されている数値は、ネストされたループ結合のすべての反復で各演算子によって生成された行の総数を示しています。 SSMSでは、各演算子の実際の行数プロパティを確認する必要があります。
一番上のscan-aggregate-filterブランチの場合、この合計は1,312,033,284行です。 2番目のブランチは、追加の436,185,324行を提供します。一致する行を見つけるために-2分と-3分のケースが必要になった場合は、さらに悪くなります。うまくいけば、「単純なクエリ」が20分間実行される理由を確認できます。
_retrieved_at
_にインデックスを作成します。
_CREATE NONCLUSTERED INDEX i2
ON dbo.visits_dist_summary_ACC_12345 (retrieved_at);
_
_account_id
_でクラスター化インデックスを評価します。
_CREATE CLUSTERED INDEX i1
ON dbo.visits_dist_summary_ACC_12345 (account_id);
_
上記の手順により、特に_retrieved_at
_(日時として入力)のインデックスが大幅に改善されます。
オプティマイザの制限/優先度のため、MAX
の計算を4回回避するためにクエリの書き換えが必要になる場合がありますが、インデックスはその操作を簡単にする必要があります(インデックスの最後から1行を読み取る)。実際に必要です。
便利な場合のクエリ書き換えアプローチの1つは次のとおりです。
_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)
);
_
上記のデータ型とインデックスの変更を伴うこのクエリの推定プラン: