web-dev-qa-db-ja.com

SUM(CASE)とCTE PIVOTのどちらが速いですか?

PIVOTを実行するには2種類の方法があります。 SQL Server 2005より前は、PIVOTが導入されたとき、ほとんどの人がこれを行いました。

_SELECT RateID
              SUM(CASE WHEN RateItemTypeID = 1 THEN UnitPrice ELSE 0 END),
              SUM(CASE WHEN RateItemTypeID = 2 THEN UnitPrice ELSE 0 END),
              SUM(CASE WHEN RateItemTypeID = 3 THEN UnitPrice ELSE 0 END)
              FROM rate_item WHERE _WhereClause_
              GROUP BY RateID
_

その後、2005年にPIVOTが導入されたとき、次のようになりました。

_   SELECT RateID, [1], [2], [3]
          FROM PertinentRates -- PertinentRates is a CTE with WHERE clause applied
          PIVOT (SUM(UnitPrice) FOR RateItemTypeID IN ([1], [2], [3])) PVT)
_

SQL Server 2005、2008 R2、2012および2014(私が使用したSQL ServerのバージョンでPIVOTを実装しています)全体で、私の経験では、常にSUM(CASE)より高速でしたいくつかのケースでは同等に高速です。 PIVOTが遅い例はありますか?

DDLは自分の仕事の例なので、与えることはできません。しかし、テーブルはかなりシンプルです。 PIVOTの例では、CTEから描画していますが、SUM(CASE)はテーブルから直接描画しています。ただし、SUM(CASE)はCTEから同じ描画を実行します。

私の作業例では、PIVOTは10秒で戻りますが、SUM(CASE)は14で戻ります。明らかに、裏で何か別のことをしている必要があります。プランは同じで、全体の50%です。クエリアナライザーでPIVOTSUM(CASE)に変換されます。しかし、SUM(CASE)は13秒未満で戻ってくることはなく、PIVOTは11秒以上戻ってくることはありません。

私はそれらを前後に実行してみましたが、それらが実行される順序は問題ではありません。両方をコールドキャッシュから実行すると、どちらにも時間がかかりますが、PIVOTの方が高速です(12対17)秒。 2番目のサーバーでは再現できませんが、その方がかなり優れています。わずか5秒で、それぞれ5秒です。 PIVOTは少し優れていますが、パーセンテージでは最初のサーバーと同じEdgeがありません。

IO統計は、クエリプランと同様に、2つの間で同一です。それは奇妙なことですが、異なるIO統計が私はこの特定の例のためにそれらを見たことがありません。

7
Matthew Sontum

PIVOTが遅い例はありますか?

これはありそうもない単純な場合。 Itzik Ben-GanがSQL Server Proの記事で指摘しているように、PIVOTクエリのプランを見ると Pivoting Data (強調が追加されています):

図3は、PIVOTクエリのプランを示しています。ご覧のとおり、このプランは標準ソリューションのプランと非常によく似ています。そのため、集計演算子のプロパティを定義済みの値の下で見ると、SQL ServerがCASEを構築していることがわかります。舞台裏の表現:


[Expr1022] = Scalar Operator(SUM(CASE WHEN [InsideTSQL2008].[Sales].[Orders].[shipcity]=N'Barcelona' THEN [InsideTSQL2008].[Sales].[Orders].[freight] ELSE NULL END))

これを念頭に置いて、PIVOT演算子に基づくソリューションが標準のソリューションよりも高いパフォーマンスを期待することはできません。現時点でのPIVOT演算子の主な利点は、冗長性が低いことです。

(非標準)PIVOT構文が直接サポートしていないより高度なピボット要件については、回避策が必要です。これらmayまたはmay notは、実装者のスキルレベルを含むさまざまな要因によっては、CASEと比較してパフォーマンスが低下します。

これらの問題のあるケースの例は、Itzikの記事で説明されています。また、Robert SheldonのSimple Talkの記事 SQL Serverでのデータのピボットについての質問は恥ずかしがり屋です でも詳しく説明されています。

私の経験では、PIVOTAgg(CASE...は、両方が最適に記述されている場合、非常に近いパフォーマンス特性を持つ非常に類似したプランを生成します。私の通常のアドバイスは、あなたにとって最も自然な構文を使用してクエリを記述し、パフォーマンスが許容できない場合にのみリライトを試みることです。

内部

SQL Serverクエリプロセッサdoesにはピボットが組み込まれているlogical演算子(LogOp_Pivot)であるため、正しくない可能性がありますquite正しいSQL Serverは、少なくともコストベースの最適化の前に行われる解析とコンパイルのアクティビティについて話している場合は、ピボットを集計とケース式にリライトすると言います(簡単なプランはピボットクエリでは使用できません)。

一方、オプティマイザができる唯一の方法implementLogOp_Pivotを含むクエリツリーは、探索ルールExpandPivotを介することです。このルールは、LogOp_Pivotを、関連するスカラー式を使用して、通常のグループ化された集約(LogOp_GbAgg)に展開します。このルールが無効になっている場合、ピボットクエリはコンパイルに失敗します。

実際には、実行可能プランを作成する前に、ピボットは常に(最終的には)集約およびスカラー式として「書き換え」られると言えます。

とにかく、LogOp_GbAggへの書き換えの結果は、実行可能なプランに必要なphysical演算子に変換されます。これは、通常のgroup-by集計実装ルールGbAggToHS(ハッシュ)またはGbAggToStrm(ストリーム)。

補足として、「手動ピボット」(ケース式の集計)に集計の下に追加の計算スカラーがある理由は、プロジェクト正規化(コンパイルの初期段階)中にケース式がクエリツリーのリーフレベルに向かってプッシュされるためです。コストベースの最適化前)。

コストベースの最適化中にPIVOTが実行されるまで式が作成されないため、ExpandPivot構文を使用するクエリにはこれがありません。プロジェクトの正規化が実行される(以前の)時点では、クエリツリーにはLogOp_Pivot要素がまだあるため、プッシュダウンする予測はなく、ケース式は通常、ハッシュまたはストリームの集約の内部になります。

SQL Server 2005以降では、式の評価は通常、後の演算子が結果を必要とするまで遅延されるため、通常、Compute Scalarを回避しても利点はありません。この場合、ケース式の評価は、集約(ハッシュまたはストリーム)が必要とするまで延期されます。

20
Paul White 9

テストを繰り返す クロスタブとピボット、パート1 –行を列に変換-Jeff Moden著、2010/08/06(初版:2008/08/19) rextester

残念ながら、私はIOの統計、時間または実行計画の実行計画にアクセスすることはできませんが、ここには誰もがいじって調査できる共通のテスト環境であるという独特の利点があります。何が起こっているのかを正確に調査および調査することに関して、これはまだ望ましいことではないことに気づきますが、テスト環境を共有できることは、このディスカッションの重要な側面だと思います。


シンプルな

rextester: http://rextester.com/BAZMGJ69528

これは@MartinSmithに追加されたものであり、クエリは同じ記事からプルされていますが、次のような元のテストにはありませんでした。

create table #timer (what varchar(64), ended datetime);
insert into #timer values ('Start',getdate());
go

SELECT TOP 400000 --<<Look!  Change this number for testing different size tables
        RowNum       = IDENTITY(INT,1,1),
        Company      = CHAR(ABS(CHECKSUM(NEWID()))%2+65)
                     + CHAR(ABS(CHECKSUM(NEWID()))%2+65)
                     + CHAR(ABS(CHECKSUM(NEWID()))%2+65),
        Amount       = CAST(ABS(CHECKSUM(NEWID()))%1000000/100.0 AS MONEY),
        Quantity     = ABS(CHECKSUM(NEWID()))%50000+1,
        Date         = CAST(Rand(CHECKSUM(NEWID()))*3653.0+36524.0 AS DATETIME),
        Year         = CAST(NULL AS SMALLINT),
        Quarter      = CAST(NULL AS TINYINT)
   INTO #SomeTable3
   FROM Master.sys.SysColumns t1
  CROSS JOIN
        Master.sys.SysColumns t2 

--===== Fill in the Year and Quarter columns from the Date column
 UPDATE #SomeTable3
    SET Year    = DATEPART(yy,Date),
        Quarter = DATEPART(qq,Date)

--===== A table is not properly formed unless a Primary Key has been assigned
     -- Takes about 1 second to execute.
  ALTER TABLE #SomeTable3
        ADD PRIMARY KEY CLUSTERED (RowNum)
CREATE NONCLUSTERED INDEX IX_#SomeTable3_CoverYear 
    ON dbo.#SomeTable3 (Year)
       INCLUDE (Amount, Quantity, Quarter) 

create statistics syear on #sometable3(year) with fullscan, norecompute;
create statistics syearquarter on #sometable3(year,quarter) with fullscan, norecompute;
GO
insert into #timer values ('Finished Loading Test Data',getdate());
go
--===== Simple Pivot 
 SELECT Year, 
        COALESCE([1],0) AS [1st Qtr],
        COALESCE([2],0) AS [2nd Qtr],
        COALESCE([3],0) AS [3rd Qtr],
        COALESCE([4],0) AS [4th Qtr],
        COALESCE([1],0) + COALESCE([2] ,0) + COALESCE([3],0) + COALESCE([4],0) AS Total
   into #SimplePivot_prep
   FROM (SELECT Year, Quarter,Amount FROM #SomeTable3)  AS src 
  PIVOT (SUM(Amount) FOR Quarter IN ([1],[2],[3],[4])) AS pvt 
go
--===== Simple Cross Tab
 SELECT Year,
        SUM(CASE WHEN Quarter = 1 THEN Amount ELSE 0 END) AS [1st Qtr],
        SUM(CASE WHEN Quarter = 2 THEN Amount ELSE 0 END) AS [2nd Qtr],
        SUM(CASE WHEN Quarter = 3 THEN Amount ELSE 0 END) AS [3rd Qtr],
        SUM(CASE WHEN Quarter = 4 THEN Amount ELSE 0 END) AS [4th Qtr],
        SUM(Amount) AS Total
   into #simpleCrossTab_prep
   FROM #SomeTable3
  GROUP BY Year
go
--insert into #timer values ('Simple Cross Tab',getdate());
go
--=====--
insert into #timer values ('Finished Prep',getdate());
go
--=====--
--===== Simple Pivot
 SELECT Year, 
        COALESCE([1],0) AS [1st Qtr],
        COALESCE([2],0) AS [2nd Qtr],
        COALESCE([3],0) AS [3rd Qtr],
        COALESCE([4],0) AS [4th Qtr],
        COALESCE([1],0) + COALESCE([2] ,0) + COALESCE([3],0) + COALESCE([4],0) AS Total
   into #SimplePivot
   FROM (SELECT Year, Quarter,Amount FROM #SomeTable3)  AS src 
  PIVOT (SUM(Amount) FOR Quarter IN ([1],[2],[3],[4])) AS pvt 
go
insert into #timer values ('Simple Pivot',getdate());
go
--=====--
--===== Simple Cross Tab
 SELECT Year,
        SUM(CASE WHEN Quarter = 1 THEN Amount ELSE 0 END) AS [1st Qtr],
        SUM(CASE WHEN Quarter = 2 THEN Amount ELSE 0 END) AS [2nd Qtr],
        SUM(CASE WHEN Quarter = 3 THEN Amount ELSE 0 END) AS [3rd Qtr],
        SUM(CASE WHEN Quarter = 4 THEN Amount ELSE 0 END) AS [4th Qtr],
        SUM(Amount) AS Total
   into #simpleCrossTab
   FROM #SomeTable3
  GROUP BY Year
go 
insert into #timer values ('Simple Cross Tab',getdate());
go
--=====--
select 
    o.what
  , started=isnull(convert(varchar(30),x.ended),o.ended)
  , ended=convert(varchar(30),o.ended)
  , DurationInMs=datediff(millisecond,x.ended,o.ended)
from #timer o
  outer apply (select top 1 ended from #timer i where i.ended < o.ended order by i.ended desc) as x

戻り値:

+----------------------------+---------------------+---------------------+--------------+
|            what            |       started       |        ended        | DurationInMs |
+----------------------------+---------------------+---------------------+--------------+
| Start                      | Feb 19 2017  7:13PM | Feb 19 2017  7:13PM | NULL         |
| Finished Loading Test Data | Feb 19 2017  7:13PM | Feb 19 2017  7:13PM | 7210         |
| Finished Prep              | Feb 19 2017  7:13PM | Feb 19 2017  7:13PM | 700          |
| Simple Pivot               | Feb 19 2017  7:13PM | Feb 19 2017  7:13PM | 340          |
| Simple Cross Tab           | Feb 19 2017  7:13PM | Feb 19 2017  7:13PM | 386          |
+----------------------------+---------------------+---------------------+--------------+

pivot構文の残りすべてのテスト制限は、単一のクロス集計クエリが複数のpivotsを必要とするクエリを実行できるためです。

正常

rextester: http://rextester.com/UVZE879

create table #timer (what varchar(64), ended datetime);
insert into #timer values ('Start',getdate());
go

 SELECT TOP 300000 --<<Look!  Change this number for testing different size tables
        RowNum       = IDENTITY(INT,1,1),
        Company      = CHAR(ABS(CHECKSUM(NEWID()))%2+65)
                     + CHAR(ABS(CHECKSUM(NEWID()))%2+65)
                     + CHAR(ABS(CHECKSUM(NEWID()))%2+65),
        Amount       = CAST(ABS(CHECKSUM(NEWID()))%1000000/100.0 AS MONEY),
        Quantity     = ABS(CHECKSUM(NEWID()))%50000+1,
        Date         = CAST(Rand(CHECKSUM(NEWID()))*3653.0+36524.0 AS DATETIME),
        Year         = CAST(NULL AS SMALLINT),
        Quarter      = CAST(NULL AS TINYINT)
   INTO #SomeTable3
   FROM Master.sys.SysColumns t1
  CROSS JOIN
        Master.sys.SysColumns t2 

--===== Fill in the Year and Quarter columns from the Date column
 UPDATE #SomeTable3
    SET Year    = DATEPART(yy,Date),
        Quarter = DATEPART(qq,Date)

--===== A table is not properly formed unless a Primary Key has been assigned
     -- Takes about 1 second to execute.
  ALTER TABLE #SomeTable3
        ADD PRIMARY KEY CLUSTERED (RowNum)
CREATE NONCLUSTERED INDEX IX_#SomeTable3_Cover1 
    ON dbo.#SomeTable3 (Company, Year)
       INCLUDE (Amount, Quantity, Quarter) 

create statistics scompanyyear on #sometable3(company, year) with fullscan, norecompute;
GO
insert into #timer values ('Finished Loading Test Data',getdate());
go
--=====--
--===== "Normal" Pivot
 SELECT amt.Company,
        amt.Year,
        COALESCE(amt.[1],0) AS Q1Amt,
        COALESCE(qty.[1],0) AS Q1Qty,
        COALESCE(amt.[2],0) AS Q2Amt,
        COALESCE(qty.[2],0) AS Q2Qty,
        COALESCE(amt.[3],0) AS Q3Amt,
        COALESCE(qty.[3],0) AS Q3Qty,
        COALESCE(amt.[4],0) AS Q4Amt,
        COALESCE(qty.[4],0) AS Q5Qty,
        COALESCE(amt.[1],0)+COALESCE(amt.[2],0)+COALESCE(amt.[3],0)+COALESCE(amt.[4],0) AS TotalAmt,
        COALESCE(qty.[1],0)+COALESCE(qty.[2],0)+COALESCE(qty.[3],0)+COALESCE(qty.[4],0) AS TotalQty
   into #NormalPivot_prep
   FROM (SELECT Company, Year, Quarter, Amount FROM #SomeTable3) t1
        PIVOT (SUM(Amount) FOR Quarter IN ([1], [2], [3], [4])) AS amt
  INNER JOIN 
        (SELECT Company, Year, Quarter, Quantity FROM #SomeTable3) t2
        PIVOT (SUM(Quantity) FOR Quarter IN ([1], [2], [3], [4])) AS qty
     ON qty.Company = amt.Company 
    AND qty.Year    = amt.Year         
  ORDER BY amt.Company, amt.Year
go
--insert into #timer values ('Finished Normal Pivot',getdate());
go
--=====--
--===== "Normal" Cross Tab
 SELECT Company,
        Year,
        SUM(CASE WHEN Quarter = 1 THEN Amount   ELSE 0 END) AS Q1Amt,
        SUM(CASE WHEN Quarter = 1 THEN Quantity ELSE 0 END) AS Q1Qty,
        SUM(CASE WHEN Quarter = 2 THEN Amount   ELSE 0 END) AS Q2Amt,
        SUM(CASE WHEN Quarter = 2 THEN Quantity ELSE 0 END) AS Q2Qty,
        SUM(CASE WHEN Quarter = 3 THEN Amount   ELSE 0 END) AS Q3Amt,
        SUM(CASE WHEN Quarter = 3 THEN Quantity ELSE 0 END) AS Q3Qty,
        SUM(CASE WHEN Quarter = 4 THEN Amount   ELSE 0 END) AS Q4Amt,
        SUM(CASE WHEN Quarter = 4 THEN Quantity ELSE 0 END) AS Q4Qty,
        SUM(Amount)   AS TotalAmt,
        SUM(Quantity) AS TotalQty
   into #NormalCrossTab_prep
   FROM #SomeTable3
  GROUP BY Company, Year
  ORDER BY Company, Year
go
--insert into #timer values ('Finished Normal Cross Tab',getdate());
insert into #timer values ('Finished Prep',getdate());
go
--=====--
--===== "Normal" Pivot
 SELECT amt.Company,
        amt.Year,
        COALESCE(amt.[1],0) AS Q1Amt,
        COALESCE(qty.[1],0) AS Q1Qty,
        COALESCE(amt.[2],0) AS Q2Amt,
        COALESCE(qty.[2],0) AS Q2Qty,
        COALESCE(amt.[3],0) AS Q3Amt,
        COALESCE(qty.[3],0) AS Q3Qty,
        COALESCE(amt.[4],0) AS Q4Amt,
        COALESCE(qty.[4],0) AS Q5Qty,
        COALESCE(amt.[1],0)+COALESCE(amt.[2],0)+COALESCE(amt.[3],0)+COALESCE(amt.[4],0) AS TotalAmt,
        COALESCE(qty.[1],0)+COALESCE(qty.[2],0)+COALESCE(qty.[3],0)+COALESCE(qty.[4],0) AS TotalQty
   into #NormalPivot
   FROM (SELECT Company, Year, Quarter, Amount FROM #SomeTable3) t1
        PIVOT (SUM(Amount) FOR Quarter IN ([1], [2], [3], [4])) AS amt
  INNER JOIN 
        (SELECT Company, Year, Quarter, Quantity FROM #SomeTable3) t2
        PIVOT (SUM(Quantity) FOR Quarter IN ([1], [2], [3], [4])) AS qty
     ON qty.Company = amt.Company 
    AND qty.Year    = amt.Year         
  ORDER BY amt.Company, amt.Year
go
insert into #timer values ('Finished Normal Pivot',getdate());
go
--=====--
--===== "Normal" Cross Tab
 SELECT Company,
        Year,
        SUM(CASE WHEN Quarter = 1 THEN Amount   ELSE 0 END) AS Q1Amt,
        SUM(CASE WHEN Quarter = 1 THEN Quantity ELSE 0 END) AS Q1Qty,
        SUM(CASE WHEN Quarter = 2 THEN Amount   ELSE 0 END) AS Q2Amt,
        SUM(CASE WHEN Quarter = 2 THEN Quantity ELSE 0 END) AS Q2Qty,
        SUM(CASE WHEN Quarter = 3 THEN Amount   ELSE 0 END) AS Q3Amt,
        SUM(CASE WHEN Quarter = 3 THEN Quantity ELSE 0 END) AS Q3Qty,
        SUM(CASE WHEN Quarter = 4 THEN Amount   ELSE 0 END) AS Q4Amt,
        SUM(CASE WHEN Quarter = 4 THEN Quantity ELSE 0 END) AS Q4Qty,
        SUM(Amount)   AS TotalAmt,
        SUM(Quantity) AS TotalQty
   into #NormalCrossTab
   FROM #SomeTable3
  GROUP BY Company, Year
  ORDER BY Company, Year
go
insert into #timer values ('Finished Normal Cross Tab',getdate());
go
--=====--
select 
    o.what
  , started=isnull(convert(varchar(30),x.ended),o.ended)
  , ended=convert(varchar(30),o.ended)
  , DurationInMs=datediff(millisecond,x.ended,o.ended)
from #timer o
  outer apply (select top 1 ended from #timer i where i.ended < o.ended order by i.ended desc) as x

戻り値:

+----------------------------+---------------------+---------------------+--------------+
|            what            |       started       |        ended        | DurationInMs |
+----------------------------+---------------------+---------------------+--------------+
| Start                      | Feb 19 2017  7:19PM | Feb 19 2017  7:19PM | NULL         |
| Finished Loading Test Data | Feb 19 2017  7:19PM | Feb 19 2017  7:19PM | 5260         |
| Finished Prep              | Feb 19 2017  7:19PM | Feb 19 2017  7:19PM | 1003         |
| Finished Normal Pivot      | Feb 19 2017  7:19PM | Feb 19 2017  7:19PM | 550          |
| Finished Normal Cross Tab  | Feb 19 2017  7:19PM | Feb 19 2017  7:19PM | 513          |
+----------------------------+---------------------+---------------------+--------------+

「事前集計」

rextester: http://rextester.com/WBGUYR51251

create table #timer (what varchar(64), ended datetime);
insert into #timer values ('Start',getdate());
go
SELECT TOP 300000 --<<Look!  Change this number for testing different size tables
        RowNum       = IDENTITY(INT,1,1),
        Company      = CHAR(ABS(CHECKSUM(NEWID()))%2+65)
                     + CHAR(ABS(CHECKSUM(NEWID()))%2+65)
                     + CHAR(ABS(CHECKSUM(NEWID()))%2+65),
        Amount       = CAST(ABS(CHECKSUM(NEWID()))%1000000/100.0 AS MONEY),
        Quantity     = ABS(CHECKSUM(NEWID()))%50000+1,
        Date         = CAST(Rand(CHECKSUM(NEWID()))*3653.0+36524.0 AS DATETIME),
        Year         = CAST(NULL AS SMALLINT),
        Quarter      = CAST(NULL AS TINYINT)
   INTO #SomeTable3
   FROM Master.sys.SysColumns t1
  CROSS JOIN
        Master.sys.SysColumns t2 

--===== Fill in the Year and Quarter columns from the Date column
 UPDATE #SomeTable3
    SET Year    = DATEPART(yy,Date),
        Quarter = DATEPART(qq,Date)

--===== A table is not properly formed unless a Primary Key has been assigned
     -- Takes about 1 second to execute.
  ALTER TABLE #SomeTable3
        ADD PRIMARY KEY CLUSTERED (RowNum)
CREATE NONCLUSTERED INDEX IX_#SomeTable3_Cover1 
    ON dbo.#SomeTable3 (Company, Year)
       INCLUDE (Amount, Quantity, Quarter) 

create statistics scompanyyear on #sometable3(company, year) with fullscan, norecompute;
GO
insert into #timer values ('Finished Loading Test Data',getdate());
go
--=====--
--===== "Pre-aggregated" Pivot
SELECT amt.Company,
        amt.Year,
        COALESCE(amt.[1],0) AS Q1Amt,
        COALESCE(qty.[1],0) AS Q1Qty,
        COALESCE(amt.[2],0) AS Q2Amt,
        COALESCE(qty.[2],0) AS Q2Qty,
        COALESCE(amt.[3],0) AS Q3Amt,
        COALESCE(qty.[3],0) AS Q3Qty,
        COALESCE(amt.[4],0) AS Q4Amt,
        COALESCE(qty.[4],0) AS Q5Qty,
        COALESCE(amt.[1],0)+COALESCE(amt.[2],0)+COALESCE(amt.[3],0)+COALESCE(amt.[4],0) AS TotalAmt,
        COALESCE(qty.[1],0)+COALESCE(qty.[2],0)+COALESCE(qty.[3],0)+COALESCE(qty.[4],0) AS TotalQty
   into #preA_Pivot_prep
   FROM (SELECT Company, Year, Quarter, SUM(Amount) AS Amount FROM #SomeTable3 GROUP BY Company, Year, Quarter) t1
        PIVOT (SUM(Amount) FOR Quarter IN ([1], [2], [3], [4])) AS amt
  INNER JOIN 
        (SELECT Company, Year, Quarter, SUM(Quantity) AS Quantity FROM #SomeTable3 GROUP BY Company, Year, Quarter) t2
        PIVOT (SUM(Quantity) FOR Quarter IN ([1], [2], [3], [4])) AS qty
     ON qty.Company = amt.Company 
    AND qty.Year    = amt.Year         
  ORDER BY amt.Company, amt.Year
go
--insert into #timer values ('Finished "Pre-aggregated" Pivot',getdate());
go
--=====--
--===== "Pre-aggregated" Cross Tab
SELECT Company,
        Year,
        SUM(CASE WHEN Quarter = 1 THEN Amount   ELSE 0 END) AS Q1Amt,
        SUM(CASE WHEN Quarter = 1 THEN Quantity ELSE 0 END) AS Q1Qty,
        SUM(CASE WHEN Quarter = 2 THEN Amount   ELSE 0 END) AS Q2Amt,
        SUM(CASE WHEN Quarter = 2 THEN Quantity ELSE 0 END) AS Q2Qty,
        SUM(CASE WHEN Quarter = 3 THEN Amount   ELSE 0 END) AS Q3Amt,
        SUM(CASE WHEN Quarter = 3 THEN Quantity ELSE 0 END) AS Q3Qty,
        SUM(CASE WHEN Quarter = 4 THEN Amount   ELSE 0 END) AS Q4Amt,
        SUM(CASE WHEN Quarter = 4 THEN Quantity ELSE 0 END) AS Q4Qty,
        SUM(Amount)   AS TotalAmt,
        SUM(Quantity) AS TotalQty
   into #preA_CrossTab_prep
   FROM (SELECT Company,Year,Quarter,SUM(Amount) AS Amount,SUM(Quantity) AS Quantity
           FROM #SomeTable3 GROUP BY Company,Year,Quarter) d
  GROUP BY Company, Year
  ORDER BY Company, Year
go
--insert into #timer values ('Finished "Pre-aggregated" Cross Tab',getdate());
go
--=====--
insert into #timer values ('Finished Prep',getdate());
--=====--
--===== "Pre-aggregated" Pivot
SELECT amt.Company,
        amt.Year,
        COALESCE(amt.[1],0) AS Q1Amt,
        COALESCE(qty.[1],0) AS Q1Qty,
        COALESCE(amt.[2],0) AS Q2Amt,
        COALESCE(qty.[2],0) AS Q2Qty,
        COALESCE(amt.[3],0) AS Q3Amt,
        COALESCE(qty.[3],0) AS Q3Qty,
        COALESCE(amt.[4],0) AS Q4Amt,
        COALESCE(qty.[4],0) AS Q5Qty,
        COALESCE(amt.[1],0)+COALESCE(amt.[2],0)+COALESCE(amt.[3],0)+COALESCE(amt.[4],0) AS TotalAmt,
        COALESCE(qty.[1],0)+COALESCE(qty.[2],0)+COALESCE(qty.[3],0)+COALESCE(qty.[4],0) AS TotalQty
   into #preA_Pivot
   FROM (SELECT Company, Year, Quarter, SUM(Amount) AS Amount FROM #SomeTable3 GROUP BY Company, Year, Quarter) t1
        PIVOT (SUM(Amount) FOR Quarter IN ([1], [2], [3], [4])) AS amt
  INNER JOIN 
        (SELECT Company, Year, Quarter, SUM(Quantity) AS Quantity FROM #SomeTable3 GROUP BY Company, Year, Quarter) t2
        PIVOT (SUM(Quantity) FOR Quarter IN ([1], [2], [3], [4])) AS qty
     ON qty.Company = amt.Company 
    AND qty.Year    = amt.Year         
  ORDER BY amt.Company, amt.Year
go
insert into #timer values ('Finished "Pre-aggregated" Pivot',getdate());
go
--=====--
--===== "Pre-aggregated" Cross Tab
SELECT Company,
        Year,
        SUM(CASE WHEN Quarter = 1 THEN Amount   ELSE 0 END) AS Q1Amt,
        SUM(CASE WHEN Quarter = 1 THEN Quantity ELSE 0 END) AS Q1Qty,
        SUM(CASE WHEN Quarter = 2 THEN Amount   ELSE 0 END) AS Q2Amt,
        SUM(CASE WHEN Quarter = 2 THEN Quantity ELSE 0 END) AS Q2Qty,
        SUM(CASE WHEN Quarter = 3 THEN Amount   ELSE 0 END) AS Q3Amt,
        SUM(CASE WHEN Quarter = 3 THEN Quantity ELSE 0 END) AS Q3Qty,
        SUM(CASE WHEN Quarter = 4 THEN Amount   ELSE 0 END) AS Q4Amt,
        SUM(CASE WHEN Quarter = 4 THEN Quantity ELSE 0 END) AS Q4Qty,
        SUM(Amount)   AS TotalAmt,
        SUM(Quantity) AS TotalQty
   into #preA_CrossTab
   FROM (SELECT Company,Year,Quarter,SUM(Amount) AS Amount,SUM(Quantity) AS Quantity
           FROM #SomeTable3 GROUP BY Company,Year,Quarter) d
  GROUP BY Company, Year
  ORDER BY Company, Year
go
insert into #timer values ('Finished "Pre-aggregated" Cross Tab',getdate());
go
--=====--
select 
    o.what
  , started=isnull(convert(varchar(30),x.ended),o.ended)
  , ended=convert(varchar(30),o.ended)
  , DurationInMs=datediff(millisecond,x.ended,o.ended)
from #timer o
  outer apply (select top 1 ended from #timer i where i.ended < o.ended order by i.ended desc) as x

戻り値:

+-------------------------------------+---------------------+---------------------+--------------+
|                what                 |       started       |        ended        | DurationInMs |
+-------------------------------------+---------------------+---------------------+--------------+
| Start                               | Feb 19 2017  7:23PM | Feb 19 2017  7:23PM | NULL         |
| Finished Loading Test Data          | Feb 19 2017  7:23PM | Feb 19 2017  7:23PM | 5440         |
| Finished Prep                       | Feb 19 2017  7:23PM | Feb 19 2017  7:23PM | 1513         |
| Finished "Pre-aggregated" Pivot     | Feb 19 2017  7:23PM | Feb 19 2017  7:23PM | 683          |
| Finished "Pre-aggregated" Cross Tab | Feb 19 2017  7:23PM | Feb 19 2017  7:23PM | 370          |
+-------------------------------------+---------------------+---------------------+--------------+

CTEによる「事前集計」

rextester: http://rextester.com/WCTJH5484

create table #timer (what varchar(64), ended datetime);
insert into #timer values ('Start',getdate());
go

SELECT TOP 300000 --<<Look!  Change this number for testing different size tables
        RowNum       = IDENTITY(INT,1,1),
        Company      = CHAR(ABS(CHECKSUM(NEWID()))%2+65)
                     + CHAR(ABS(CHECKSUM(NEWID()))%2+65)
                     + CHAR(ABS(CHECKSUM(NEWID()))%2+65),
        Amount       = CAST(ABS(CHECKSUM(NEWID()))%1000000/100.0 AS MONEY),
        Quantity     = ABS(CHECKSUM(NEWID()))%50000+1,
        Date         = CAST(Rand(CHECKSUM(NEWID()))*3653.0+36524.0 AS DATETIME),
        Year         = CAST(NULL AS SMALLINT),
        Quarter      = CAST(NULL AS TINYINT)
   INTO #SomeTable3
   FROM Master.sys.SysColumns t1
  CROSS JOIN
        Master.sys.SysColumns t2 

--===== Fill in the Year and Quarter columns from the Date column
 UPDATE #SomeTable3
    SET Year    = DATEPART(yy,Date),
        Quarter = DATEPART(qq,Date)

--===== A table is not properly formed unless a Primary Key has been assigned
     -- Takes about 1 second to execute.
  ALTER TABLE #SomeTable3
        ADD PRIMARY KEY CLUSTERED (RowNum)
CREATE NONCLUSTERED INDEX IX_#SomeTable3_Cover1 
    ON dbo.#SomeTable3 (Company, Year)
       INCLUDE (Amount, Quantity, Quarter) 

create statistics syearquarter on #sometable3(year,quarter) with fullscan, norecompute;
GO
insert into #timer values ('Finished Loading Test Data',getdate());
go
--=====--
--===== "Pre-aggregated" Pivot with CTE
;WITH
ctePreAgg AS
(SELECT Company,Year,Quarter,SUM(Amount) AS Amount,SUM(Quantity) AS Quantity
   FROM #SomeTable3 
  GROUP BY Company,Year,Quarter
)
 SELECT amt.Company,
        amt.Year,
        COALESCE(amt.[1],0) AS Q1Amt,
        COALESCE(qty.[1],0) AS Q1Qty,
        COALESCE(amt.[2],0) AS Q2Amt,
        COALESCE(qty.[2],0) AS Q2Qty,
        COALESCE(amt.[3],0) AS Q3Amt,
        COALESCE(qty.[3],0) AS Q3Qty,
        COALESCE(amt.[4],0) AS Q4Amt,
        COALESCE(qty.[4],0) AS Q5Qty,
        COALESCE(amt.[1],0)+COALESCE(amt.[2],0)+COALESCE(amt.[3],0)+COALESCE(amt.[4],0) AS TotalAmt,
        COALESCE(qty.[1],0)+COALESCE(qty.[2],0)+COALESCE(qty.[3],0)+COALESCE(qty.[4],0) AS TotalQty
   into #prea_Pivot_wcte_prep
   FROM (SELECT Company, Year, Quarter, Amount FROM ctePreAgg) AS t1
        PIVOT (SUM(Amount) FOR Quarter IN ([1], [2], [3], [4])) AS amt
  INNER JOIN 
        (SELECT Company, Year, Quarter, Quantity FROM ctePreAgg) AS t2
        PIVOT (SUM(Quantity) FOR Quarter IN ([1], [2], [3], [4])) AS qty
     ON qty.Company = amt.Company 
    AND qty.Year    = amt.Year         
  ORDER BY amt.Company, amt.Year
go
--insert into #timer values ('Finished "Pre-aggregated" Pivot with CTE',getdate());
go
--=====--
--===== "Pre-aggregated" Cross Tab with CTE
;WITH
ctePreAgg AS
(SELECT Company,Year,Quarter,SUM(Amount) AS Amount,SUM(Quantity) AS Quantity
   FROM #SomeTable3 
  GROUP BY Company,Year,Quarter
)
 SELECT Company,
        Year,
        SUM(CASE WHEN Quarter = 1 THEN Amount   ELSE 0 END) AS Q1Amt,
        SUM(CASE WHEN Quarter = 1 THEN Quantity ELSE 0 END) AS Q1Qty,
        SUM(CASE WHEN Quarter = 2 THEN Amount   ELSE 0 END) AS Q2Amt,
        SUM(CASE WHEN Quarter = 2 THEN Quantity ELSE 0 END) AS Q2Qty,
        SUM(CASE WHEN Quarter = 3 THEN Amount   ELSE 0 END) AS Q3Amt,
        SUM(CASE WHEN Quarter = 3 THEN Quantity ELSE 0 END) AS Q3Qty,
        SUM(CASE WHEN Quarter = 4 THEN Amount   ELSE 0 END) AS Q4Amt,
        SUM(CASE WHEN Quarter = 4 THEN Quantity ELSE 0 END) AS Q4Qty,
        SUM(Amount)   AS TotalAmt,
        SUM(Quantity) AS TotalQty
   into #prea_CrossTab_wcte_prep
   FROM ctePreAgg
  GROUP BY Company, Year
  ORDER BY Company, Year
go
--insert into #timer values ('Finished "Pre-aggregated" Cross Tab with CTE',getdate());
go
--=====--
insert into #timer values ('Finished Prep',getdate());
go
--=====--
--===== "Pre-aggregated" Pivot with CTE
;WITH
ctePreAgg AS
(SELECT Company,Year,Quarter,SUM(Amount) AS Amount,SUM(Quantity) AS Quantity
   FROM #SomeTable3 
  GROUP BY Company,Year,Quarter
)
 SELECT amt.Company,
        amt.Year,
        COALESCE(amt.[1],0) AS Q1Amt,
        COALESCE(qty.[1],0) AS Q1Qty,
        COALESCE(amt.[2],0) AS Q2Amt,
        COALESCE(qty.[2],0) AS Q2Qty,
        COALESCE(amt.[3],0) AS Q3Amt,
        COALESCE(qty.[3],0) AS Q3Qty,
        COALESCE(amt.[4],0) AS Q4Amt,
        COALESCE(qty.[4],0) AS Q5Qty,
        COALESCE(amt.[1],0)+COALESCE(amt.[2],0)+COALESCE(amt.[3],0)+COALESCE(amt.[4],0) AS TotalAmt,
        COALESCE(qty.[1],0)+COALESCE(qty.[2],0)+COALESCE(qty.[3],0)+COALESCE(qty.[4],0) AS TotalQty
   into #prea_Pivot_wcte
   FROM (SELECT Company, Year, Quarter, Amount FROM ctePreAgg) AS t1
        PIVOT (SUM(Amount) FOR Quarter IN ([1], [2], [3], [4])) AS amt
  INNER JOIN 
        (SELECT Company, Year, Quarter, Quantity FROM ctePreAgg) AS t2
        PIVOT (SUM(Quantity) FOR Quarter IN ([1], [2], [3], [4])) AS qty
     ON qty.Company = amt.Company 
    AND qty.Year    = amt.Year         
  ORDER BY amt.Company, amt.Year
go
insert into #timer values ('Finished "Pre-aggregated" Pivot with CTE',getdate());
go
--=====--
--===== "Pre-aggregated" Cross Tab with CTE
;WITH
ctePreAgg AS
(SELECT Company,Year,Quarter,SUM(Amount) AS Amount,SUM(Quantity) AS Quantity
   FROM #SomeTable3 
  GROUP BY Company,Year,Quarter
)
 SELECT Company,
        Year,
        SUM(CASE WHEN Quarter = 1 THEN Amount   ELSE 0 END) AS Q1Amt,
        SUM(CASE WHEN Quarter = 1 THEN Quantity ELSE 0 END) AS Q1Qty,
        SUM(CASE WHEN Quarter = 2 THEN Amount   ELSE 0 END) AS Q2Amt,
        SUM(CASE WHEN Quarter = 2 THEN Quantity ELSE 0 END) AS Q2Qty,
        SUM(CASE WHEN Quarter = 3 THEN Amount   ELSE 0 END) AS Q3Amt,
        SUM(CASE WHEN Quarter = 3 THEN Quantity ELSE 0 END) AS Q3Qty,
        SUM(CASE WHEN Quarter = 4 THEN Amount   ELSE 0 END) AS Q4Amt,
        SUM(CASE WHEN Quarter = 4 THEN Quantity ELSE 0 END) AS Q4Qty,
        SUM(Amount)   AS TotalAmt,
        SUM(Quantity) AS TotalQty
   into #prea_CrossTab_wcte
   FROM ctePreAgg
  GROUP BY Company, Year
  ORDER BY Company, Year
go
insert into #timer values ('Finished "Pre-aggregated" Cross Tab with CTE',getdate());
go
--=====--
select 
    o.what
  , started=isnull(convert(varchar(30),x.ended),o.ended)
  , ended=convert(varchar(30),o.ended)
  , DurationInMs=datediff(millisecond,x.ended,o.ended)
from #timer o
  outer apply (select top 1 ended from #timer i where i.ended < o.ended order by i.ended desc) as x

戻り値:

+----------------------------------------------+---------------------+---------------------+--------------+
|                     what                     |       started       |        ended        | DurationInMs |
+----------------------------------------------+---------------------+---------------------+--------------+
| Start                                        | Feb 19 2017  7:25PM | Feb 19 2017  7:25PM | NULL         |
| Finished Loading Test Data                   | Feb 19 2017  7:25PM | Feb 19 2017  7:26PM | 5723         |
| Finished Prep                                | Feb 19 2017  7:26PM | Feb 19 2017  7:26PM | 950          |
| Finished "Pre-aggregated" Pivot with CTE     | Feb 19 2017  7:26PM | Feb 19 2017  7:26PM | 580          |
| Finished "Pre-aggregated" Cross Tab with CTE | Feb 19 2017  7:26PM | Feb 19 2017  7:26PM | 323          |
+----------------------------------------------+---------------------+---------------------+--------------+
12
SqlZim

誤った仮定を修正するためのメモ:

ピボットを記述する主な方法は2つではなくです。 3番目は、駆動テーブルといくつかの結合(LEFT JOINまたはOUTER/CROSS APPLYを使用)を使用しています。

これは、いくつかの詳細(テーブルの分布、インデックスなど)および特定のピボット操作の要件に応じて、多かれ少なかれ効率的です。 SUM/GROUP BYメソッドといくつかの違いがあります。

  • 適切なインデックス(適切な意味:WHERE句、GROUP BY句、および集計する列によって異なる)がある場合、テーブル全体をスキャンすることを回避できます。特定の例では、(RateItemTypeID, RateID) INCLUDE (UnitPrice)が空の場合の_WherClause_のインデックス。

    • これはいくつかの場面で有益です。
      たとえば、テーブルに数百の異なるRateItemTypeID値が含まれているが、クエリで必要なのは少数の場合のみです。テーブル全体(またはインデックス全体)をスキャンする場合と、狭いNCIの小さい部分を検索する場合とでは、2番目のほうが効率的であると思います。
    • もちろん、異なるWHERE述語と集計列に異なるインデックスが必要になるため、逆効果になる可能性があります。
  • GROUP BYと駆動サブクエリ全体でさえ、別のテーブル(特定の例ではRateテーブル)で置き換えることができます。

  • いくつかのピボットバリエーションでは、サブクエリのGROUP BYを削除して、サブクエリを単純なLEFT結合に変換することもできます(たとえば、(RateID, RateItemTypeID)UNIQUE制約がある場合)特定の場合)。これは、 "[SUM/GROUP BY]メソッドのSUMが(これらの場合)GROUP BYであり、1つの値(および複数のNull)を合計するためにのみ存在することを示しています。

クエリ:

SELECT 
    d.RateID,
    Sum1 = COALESCE(s1.Sum1, 0),
    Sum2 = COALESCE(s2.Sum2, 0),
    Sum3 = COALESCE(s3.Sum3, 0)  
FROM 
    ( SELECT RateID    -- 
      FROM rate_item 
      WHERE _WhereClause_
      GROUP BY RateID
    ) AS d             -- driving table with the DISTINCT RateID values
  OUTER APPLY
    ( SELECT Sum1 = SUM(r1.UnitPrice)
      FROM rate_item AS r1
      WHERE _WhereClause_
        AND r1.RateItemTypeID = 1 
        AND r1.RateID = d.RateID
    ) AS s1
  OUTER APPLY
    ( SELECT Sum2 = SUM(r2.UnitPrice)
      FROM rate_item AS r2
      WHERE _WhereClause_
        AND r2.RateItemTypeID = 2 
        AND r2.RateID = d.RateID
    ) AS s2
  OUTER APPLY
    ( SELECT Sum3 = SUM(r3.UnitPrice)
      FROM rate_item AS r3
      WHERE _WhereClause_
        AND r3.RateItemTypeID = 3 
        AND r3.RateID = d.RateID
    ) AS s3 ;
6
ypercubeᵀᴹ

SQL Serverはクエリの下で変換します。

SELECT 
RateID, [1], [2], [3]
FROM PertinentRates
PIVOT (SUM(UnitPrice) FOR RateItemTypeID IN ([1], [2], [3])) PVT)

に:

SELECT 
RateID
SUM(CASE WHEN RateItemTypeID = 1 THEN UnitPrice ELSE 0 END),
SUM(CASE WHEN RateItemTypeID = 2 THEN UnitPrice ELSE 0 END),
SUM(CASE WHEN RateItemTypeID = 3 THEN UnitPrice ELSE 0 END)
 FROM rate_item WHERE supplierid = 2882874 AND rateplanid = 1 AND rateitemtypeid IN (1, 2, 3)
          GROUP BY RateID

したがって、AFAIKは可読性に要約されます

以下は短いデモです:

CREATE TABLE #Sales (EmpId INT, Yr INT, Sales MONEY)
INSERT #Sales VALUES(1, 2005, 12000)
INSERT #Sales VALUES(1, 2006, 18000)
INSERT #Sales VALUES(1, 2007, 25000)
INSERT #Sales VALUES(2, 2005, 15000)
INSERT #Sales VALUES(2, 2006, 6000)
INSERT #Sales VALUES(3, 2006, 20000)
INSERT #Sales VALUES(3, 2007, 24000)

今クエリ

SELECT EmpId, [2005], [2006], [2007]
FROM (SELECT EmpId, Yr, Sales FROM #Sales) AS s
PIVOT (SUM(Sales) FOR Yr IN ([2005], [2006], [2007])) AS p    

select 
empid,
sum(Case when yr=2005 then sales end) '2005',
sum(Case when yr=2006 then sales end) '2006',
sum(Case when yr=2007 then sales end) '2007'
from
#sales
group by empid

両方のクエリがバッチで実行されると、どちらも同じコストを使用し、プランはほとんど同じになります。

enter image description here

SHOWPLAN_TEXT

ピボット

  |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1018]=(0) THEN NULL ELSE [Expr1019] END, [Expr1004]=CASE WHEN [Expr1020]=(0) THEN NULL ELSE [Expr1021] END, [Expr1005]=CASE WHEN [Expr1022]=(0) THEN NULL ELSE [Expr1023] END))
       |--Stream Aggregate(GROUP BY:([tempdb].[dbo].[#Sales].[EmpId]) DEFINE:([Expr1018]=COUNT_BIG(CASE WHEN [tempdb].[dbo].[#Sales].[Yr]=(2005) THEN [tempdb].[dbo].[#Sales].[Sales] ELSE NULL END), [Expr1019]=SUM(CASE WHEN [tempdb].[dbo].[#Sales].[Yr]=(2005) THEN [tempdb].[dbo].[#Sales].[Sales] ELSE NULL END), [Expr1020]=COUNT_BIG(CASE WHEN [tempdb].[dbo].[#Sales].[Yr]=(2006) THEN [tempdb].[dbo].[#Sales].[Sales] ELSE NULL END), [Expr1021]=SUM(CASE WHEN [tempdb].[dbo].[#Sales].[Yr]=(2006) THEN [tempdb].[dbo].[#Sales].[Sales] ELSE NULL END), [Expr1022]=COUNT_BIG(CASE WHEN [tempdb].[dbo].[#Sales].[Yr]=(2007) THEN [tempdb].[dbo].[#Sales].[Sales] ELSE NULL END), [Expr1023]=SUM(CASE WHEN [tempdb].[dbo].[#Sales].[Yr]=(2007) THEN [tempdb].[dbo].[#Sales].[Sales] ELSE NULL END)))
            |--Sort(ORDER BY:([tempdb].[dbo].[#Sales].[EmpId] ASC))
                 |--Table Scan(OBJECT:([tempdb].[dbo].[#Sales]))

場合

  |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1021]=(0) THEN NULL ELSE [Expr1022] END, [Expr1004]=CASE WHEN [Expr1023]=(0) THEN NULL ELSE [Expr1024] END, [Expr1005]=CASE WHEN [Expr1025]=(0) THEN NULL ELSE [Expr1026] END))
       |--Stream Aggregate(GROUP BY:([tempdb].[dbo].[#sales].[EmpId]) DEFINE:([Expr1021]=COUNT_BIG([Expr1006]), [Expr1022]=SUM([Expr1006]), [Expr1023]=COUNT_BIG([Expr1007]), [Expr1024]=SUM([Expr1007]), [Expr1025]=COUNT_BIG([Expr1008]), [Expr1026]=SUM([Expr1008])))
            |--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [tempdb].[dbo].[#sales].[Yr]=(2005) THEN [tempdb].[dbo].[#sales].[Sales] ELSE NULL END, [Expr1007]=CASE WHEN [tempdb].[dbo].[#sales].[Yr]=(2006) THEN [tempdb].[dbo].[#sales].[Sales] ELSE NULL END, [Expr1008]=CASE WHEN [tempdb].[dbo].[#sales].[Yr]=(2007) THEN [tempdb].[dbo].[#sales].[Sales] ELSE NULL END))
                 |--Sort(ORDER BY:([tempdb].[dbo].[#sales].[EmpId] ASC))
                      |--Table Scan(OBJECT:([tempdb].[dbo].[#sales]))
5
TheGameiswar