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%です。クエリアナライザーでPIVOT
がSUM(CASE)
に変換されます。しかし、SUM(CASE)
は13秒未満で戻ってくることはなく、PIVOT
は11秒以上戻ってくることはありません。
私はそれらを前後に実行してみましたが、それらが実行される順序は問題ではありません。両方をコールドキャッシュから実行すると、どちらにも時間がかかりますが、PIVOT
の方が高速です(12対17)秒。 2番目のサーバーでは再現できませんが、その方がかなり優れています。わずか5秒で、それぞれ5秒です。 PIVOT
は少し優れていますが、パーセンテージでは最初のサーバーと同じEdgeがありません。
IO統計は、クエリプランと同様に、2つの間で同一です。それは奇妙なことですが、異なるIO統計が私はこの特定の例のためにそれらを見たことがありません。
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でのデータのピボットについての質問は恥ずかしがり屋です でも詳しく説明されています。
私の経験では、PIVOT
とAgg(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を回避しても利点はありません。この場合、ケース式の評価は、集約(ハッシュまたはストリーム)が必要とするまで延期されます。
テストを繰り返す クロスタブとピボット、パート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
構文の残りすべてのテスト制限は、単一のクロス集計クエリが複数のpivot
sを必要とするクエリを実行できるためです。
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 |
+-------------------------------------+---------------------+---------------------+--------------+
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 |
+----------------------------------------------+---------------------+---------------------+--------------+
誤った仮定を修正するためのメモ:
ピボットを記述する主な方法は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 ;
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
両方のクエリがバッチで実行されると、どちらも同じコストを使用し、プランはほとんど同じになります。
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]))