web-dev-qa-db-ja.com

参加するとCTEが非常に遅い

以前にも似たようなものを投稿しましたが、今は違う方向からアプローチしているので、新しい質問をしました。これで大丈夫だといいのですが。

私は、親の請求に基づいて請求の合計を作成するCTEを使用しています。 SQLと詳細はここで見ることができます:

複数キーテーブルに関するCTEインデックスの推奨事項

CTEに何も欠けているとは思いませんが、大きなデータテーブル(350万行)で使用すると問題が発生します。

テーブルtblChargeShareには、InvoiceIDなど、必要なその他の情報が含まれているため、CTEをビューvwChargeShareSubChargesに配置し、テーブルに結合しました。

クエリ:

Select t.* from vwChargeShareSubCharges t
inner join 
tblChargeShare  s 
on t.CustomerID = s.CustomerID 
and t.MasterChargeID = s.ChargeID 
Where  s.ChargeID = 1291094

数ミリ秒で結果を返します。

クエリ:

Select ChargeID from tblChargeShare Where InvoiceID = 1045854

1行を返します:

1291094

しかし、クエリ:

Select t.* from vwChargeShareSubCharges t
inner join 
tblChargeShare  s 
on t.CustomerID = s.CustomerID 
and t.MasterChargeID = s.ChargeID 
Where  InvoiceID = 1045854

実行には2〜3分かかります。

実行プランを保存し、SQLSentryにロードしました。高速クエリのツリーは次のようになります。

Fast Query

遅いクエリからの計画は次のとおりです。

Slow Query

インデックスの再作成、チューニングアドバイザーを介したクエリの実行、およびサブクエリのさまざまな組み合わせを試しました。結合にPK以外のものが含まれている場合は常に、クエリは遅くなります。

私はここで同様の質問をしました:

SQL ServerクエリはWhere句に応じてタイムアウトします

CTEの代わりに子行の合計を行うために関数を使用したもの。これは、現在発生しているのと同じ問題を回避するためにCTEを使用して書き直したものです。私はその回答の回答を読みましたが、私は賢明ではありません-ヒントとパラメーターに関するいくつかの情報を読みましたが、それを機能させることができません。 CTEを使って書き直すことで問題が解決すると思っていました。数千行のtblChargeで実行すると、クエリは高速になります。

SQL 2008R2とSQL2012の両方でテスト済み

編集:

クエリを1つのステートメントに凝縮しましたが、同じ問題が解決しません。

WITH RCTE AS
(
SELECT  ParentChargeId, s.ChargeID, 1 AS Lvl, ISNULL(TotalAmount, 0) as TotalAmount,  ISNULL(s.TaxAmount, 0) as TaxAmount,  
ISNULL(s.DiscountAmount, 0) as DiscountAmount, s.CustomerID, c.ChargeID as MasterChargeID
from tblCharge c inner join tblChargeShare s
on c.ChargeID = s.ChargeID Where s.ChargeShareStatusID < 3 and ParentChargeID is NULL

UNION ALL

SELECT c.ParentChargeID, c.ChargeID, Lvl+1 AS Lvl, ISNULL(s.TotalAmount, 0),  ISNULL(s.TaxAmount, 0),  ISNULL(s.DiscountAmount, 0) , s.CustomerID 
, rc.MasterChargeID 
from tblCharge c inner join tblChargeShare s
on c.ChargeID = s.ChargeID
INNER JOIN RCTE rc ON c.PArentChargeID = rc.ChargeID and s.CustomerID = rc.CustomerID  Where s.ChargeShareStatusID < 3 
)

Select MasterChargeID as ChargeID, rcte.CustomerID, Sum(rcte.TotalAmount) as TotalCharged, Sum(rcte.TaxAmount) as TotalTax, Sum(rcte.DiscountAmount) as TotalDiscount
from RCTE inner join tblChargeShare s on rcte.ChargeID = s.ChargeID and RCTE.CustomerID = s.CustomerID 
Where InvoiceID = 1045854
Group by MasterChargeID, rcte.CustomerID 
GO

編集:もっと遊んで、私はこれを理解していません。

このクエリは瞬時(2ms)です:

Select t.* from
vwChargeShareSubCharges t
Where  t.MasterChargeID = 1291094

これには3分かかりますが:

DECLARE @ChargeID int = 1291094

Select t.* from
vwChargeShareSubCharges t
Where  t.MasterChargeID = @ChargeID

「In」に数字のヒープを入れても、クエリはすぐに実行されます。

Where  t.MasterChargeID in (1291090, 1291091, 1291092, 1291093,  1291094, 1291095, 1291096, 1291097, 1291098, 1291099, 129109)

編集2:

このサンプルデータを使用して、これを最初から複製できます。

問題を再現するために、いくつかのダミーデータを作成しました。 100,000行しか追加しなかったので、それほど重要ではありませんが、実行プランが正しくありません(SQLCMDモードで実行)。

CREATE TABLE [tblChargeTest](
[ChargeID] [int] IDENTITY(1,1) NOT NULL,
[ParentChargeID] [int] NULL,
[TotalAmount] [money] NULL,
[TaxAmount] [money] NULL,
[DiscountAmount] [money] NULL,
[InvoiceID] [int] NULL,
CONSTRAINT [PK_tblChargeTest] PRIMARY KEY CLUSTERED 
(
[ChargeID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,     ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO

Insert into tblChargeTest
(discountAmount, TotalAmount, TaxAmount)
Select ABS(CHECKSUM(NewId())) % 10, ABS(CHECKSUM(NewId())) % 100, ABS(CHECKSUM(NewId())) % 10
GO 100000

Update tblChargeTest
Set ParentChargeID = (ABS(CHECKSUM(NewId())) % 60000) + 20000
Where ChargeID = (ABS(CHECKSUM(NewId())) % 20000)
GO 5000

CREATE VIEW [vwChargeShareSubCharges] AS
WITH RCTE AS
(
SELECT  ParentChargeId, ChargeID, 1 AS Lvl, ISNULL(TotalAmount, 0) as TotalAmount,   ISNULL(TaxAmount, 0) as TaxAmount,  
ISNULL(DiscountAmount, 0) as DiscountAmount,  ChargeID as MasterChargeID
FROM tblChargeTest Where ParentChargeID is NULL

UNION ALL

SELECT rh.ParentChargeID, rh.ChargeID, Lvl+1 AS Lvl, ISNULL(rh.TotalAmount, 0),    ISNULL(rh.TaxAmount, 0),  ISNULL(rh.DiscountAmount, 0) 
, rc.MasterChargeID 
FROM tblChargeTest rh
INNER JOIN RCTE rc ON rh.PArentChargeID = rc.ChargeID --and rh.CustomerID =  rc.CustomerID 
)

Select MasterChargeID,  ParentChargeID, ChargeID, TotalAmount, TaxAmount, DiscountAmount , Lvl
FROM  RCTE r 
GO

次に、次の2つのクエリを実行します。

--Slow Query:
Declare @ChargeID int = 60900

Select *
from [vwChargeShareSubCharges]
Where MasterChargeID = @ChargeID

--Fast Query:
Select *
from [vwChargeShareSubCharges]
Where MasterChargeID = 60900
12
Molloch

ここでSQLServerが実行できる最善の方法は、ChargeIDのフィルターをビュー内の再帰CTEのアンカー部分にプッシュすることです。これにより、シークは階層を構築するために必要な唯一の行を見つけることができます。パラメータを定数値として指定すると、SQL Serverはその最適化を行うことができます(そのようなことに興味がある人のために、SelOnIteratorと呼ばれるルールを使用して):

Pushed predicate with a constant value

ローカル変数を使用する場合、これを行うことはできないため、ChargeIDの述語はビューの外でスタックします(これにより、すべてのNULL IDから始まる完全な階層が構築されます):

Stuck Predicate

変数を使用するときに最適なプランを取得する1つの方法は、実行のたびにオプティマイザーに新しいプランをコンパイルさせることです。結果のプランは、実行時に変数の特定の値に合わせて調整されます。これは、OPTION (RECOMPILE)クエリヒントを追加することで実現されます。

Declare @ChargeID int = 60900;

-- Produces a fast execution plan, at the cost of a compile on every execution
Select *
from [vwChargeShareSubCharges]
Where MasterChargeID = @ChargeID
OPTION (RECOMPILE);

2番目のオプションは、ビューをインラインテーブル関数に変更することです。これにより、フィルタリング述語の位置を明示的に指定できます。

CREATE FUNCTION [dbo].[udfChargeShareSubCharges]
(
    @ChargeID int
)
RETURNS TABLE AS RETURN
(
  WITH RCTE AS
  (
  SELECT  ParentChargeID, ChargeID, 1 AS Lvl, ISNULL(TotalAmount, 0) as TotalAmount,   ISNULL(TaxAmount, 0) as TaxAmount,  
  ISNULL(DiscountAmount, 0) as DiscountAmount,  ChargeID as MasterChargeID
  FROM tblChargeTest 
  Where ParentChargeID is NULL 
  AND ChargeID = @ChargeID -- Filter placed here explicitly

  UNION ALL

  SELECT rh.ParentChargeID, rh.ChargeID, Lvl+1 AS Lvl, ISNULL(rh.TotalAmount, 0),    ISNULL(rh.TaxAmount, 0),  ISNULL(rh.DiscountAmount, 0) 
  , rc.MasterChargeID 
  FROM tblChargeTest rh
  INNER JOIN RCTE rc ON rh.ParentChargeID = rc.ChargeID --and rh.CustomerID =  rc.CustomerID 
  )

  Select MasterChargeID,  ParentChargeID, ChargeID, TotalAmount, TaxAmount, DiscountAmount , Lvl
  FROM  RCTE r 
)

次のように使用します。

Declare @ChargeID int = 60900

select *
from dbo.udfChargeShareSubCharges(@ChargeID)

クエリは、ParentChargeIDのインデックスからも恩恵を受けることができます。

create index ix_ParentChargeID on tblChargeTest(ParentChargeID)

同様のシナリオでの同様の最適化ルールに関する別の回答があります。 ウィンドウ関数を含むパラメーター化されたT-SQLクエリの実行プランの最適化

15
Mikael Eriksson

次に解決策を見つけるために、CTEをe tempテーブルに選択し、そこから結合することをお勧めします。 CTEに参加した個人的な経験から、CTEによって生成されたデータを一時テーブルに挿入するだけでクエリが5分間返され、わずか4秒に短縮されました。私は実際に2つのCTEを結合していましたが、これはCTEがLONGテーブルに結合されている場合(特に外部結合)のすべての長時間実行クエリに当てはまると思います。


    --temp tables if needed to work with intermediate values
    If object_id('tempdb..#p') is not null
    drop table #p

    ;WITH cte as ( 
    select * from t1
    )

    select * 
    into #p
    from cte

    --then use the temp table as you would normally use the CTE
    select * from #p
8
kuklei