SUM(CASE) 或 CTE PIVOT 哪个更快?

Mat*_*tum 10 performance sql-server pivot query-performance

有两种类型的方法来执行PIVOT. 在 SQL Server 2005PIVOT推出之前,大多数人都是这样做的:

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
Run Code Online (Sandbox Code Playgroud)

后来,当 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)
Run Code Online (Sandbox Code Playgroud)

在 SQL Server 2005、2008 R2、2012 和 2014(我使用过该工具的 SQL Server 版本PIVOT)中,根据我的经验,它总是比SUM(CASE)或在某些情况下同样快。有没有PIVOT更慢的例子?

我不能给出 DDL,因为它是我工作中的一个例子。但是表格很简单。在PIVOT示例中,它是从 CTE 中绘制的,而 则SUM(CASE)是直接从表中绘制的。但是SUM(CASE)从 CTE 执行相同的绘图。

在我的工作示例中,PIVOT10 秒SUM(CASE)后返回,而 14秒后返回。显然它必须在幕后做一些不同的事情。计划相同,各占总数的 50%。在查询分析器中PIVOT转换为SUM(CASE)。然而,SUM(CASE)在不到 13 秒内PIVOT永远不会回来,在 11 秒内永远不会回来。

我试过来回运行它们,它们的运行顺序无关紧要。如果我从冷缓存中运行它们,它们都需要更长的时间,但PIVOT仍然更快,12 秒 vs 17 秒。无法在第二台服务器上重现,但那台要好得多;每个有 5 秒,有细微的变化。PIVOT好一点,但按百分比计算,它与第一台服务器上的优势不同。

与查询计划一样,IO 统计信息在两者之间是相同的。这很奇怪,我有点期望看到不同的 IO 统计数据,即使我从未在这个特定示例中查看过它们。

Pau*_*ite 23

有没有PIVOT更慢的例子?

在简单的情况下是不可能的。正如 Itzik Ben-Gan 在他的 SQL Server Pro 文章“查看查询计划时透视数据”中所指出的那样PIVOT(强调):

图 3 显示了PIVOT查询计划。如您所见,该计划与标准解决方案的计划非常相似,以至于如果您查看聚合运算符的属性,在定义的值下,您会发现 SQL ServerCASE在幕后构造了表达式:

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

考虑到这一点,您不应期望基于PIVOT运算符的解决方案的性能优于标准解决方案。PIVOT操作员目前的主要好处是它不那么冗长。

对于(非标准)PIVOT语法不直接支持的更高级的旋转要求,需要解决方法。与 相比,这些可能会可能不会导致更差的性能CASE,这取决于各种因素,包括实现者的技能水平。

Itzik 的文章中介绍了这些问题案例的示例,Robert Sheldon 的 Simple Talk 文章《关于在 SQL Server 中透视数据的问题你太害羞而不敢问》中也有很好的解释。

我的经验是,当两者都以最佳方式编写时PIVOT,会Agg(CASE...生成极其相似的具有极其接近的性能特征的计划。我通常的建议是使用您觉得最自然的任何语法编写查询,并且仅在性能不可接受的情况下才尝试重写。

内件

SQL Server 查询处理器确实有一个内置的 Pivot逻辑运算符 ( LogOp_Pivot),所以说 SQL Server 将Pivot重写为聚合和 case 表达式可能不太正确,至少如果我们谈论的是解析和编译活动放置在基于成本的优化之前(琐碎计划不适用于数据透视查询)。

另一方面,优化器实现包含查询树的唯一方法LogOp_Pivot是通过探索规则ExpandPivot。此规则扩展LogOp_PivotLogOp_GbAgg具有关联标量表达式的正常分组聚合 ( )。禁用此规则时,透视查询将无法编译。

在实践中,我们可以说在生成可执行计划之前,枢轴总是(最终)“重写”为聚合和标量表达式。

无论如何,rewrite to 的结果通过常规 group-by 聚合实现规则(hash) 或(stream)LogOp_GbAgg转换为可执行计划所需的物理运算符。GbAggToHSGbAggToStrm

作为旁注,“手动数据透视”(case 表达式上的聚合)在聚合下方有一个额外的计算标量的原因是,在 Project Normalization(编译的早期阶段,在基于成本的优化之前)。

使用该PIVOT语法的查询没有这个,因为在ExpandPivot基于成本的优化期间运行之前不会创建表达式。在(较早)项目规范化运行时,查询树仍然有LogOp_Pivot元素,因此没有要下推的投影,并且 case 表达式通常会在哈希或流聚合内结束。

避免使用计算标量通常没有任何优势,因为从 SQL Server 2005 开始,表达式计算通常会推迟到后面的运算符需要结果为止。在这种情况下,case 表达式的计算被推迟到聚合(散列或流)需要它。


Sql*_*Zim 13

重复交叉表和数据透视表中的测试,第 1 部分 – 将行转换为列 - Jeff Moden, 2010/08/06 (首次发布:2008/08/19)在 reextester 上

不幸的是,我无法访问 rextester 上的 IO、时间或执行计划的统计信息,但它具有独特的优势,它是一个通用的测试环境,这里的任何人都可以修改和检查。我意识到这在深入挖掘和调查正在发生的事情方面仍有一些不足之处,但我认为能够共享测试环境是本次讨论的一个重要方面。


简单的

雷克斯特: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
Run Code Online (Sandbox Code Playgroud)

返回:

+----------------------------+---------------------+---------------------+--------------+
|            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          |
+----------------------------+---------------------+---------------------+--------------+
Run Code Online (Sandbox Code Playgroud)

其余的所有测试限制都在pivot语法中,其中单个交叉表查询可以完成需要多个pivots 的查询。

普通的

雷克斯特:http: //rextester.com/UVZE87903

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
Run Code Online (Sandbox Code Playgroud)

返回:

+----------------------------+---------------------+---------------------+--------------+
|            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          |
+----------------------------+---------------------+---------------------+--------------+
Run Code Online (Sandbox Code Playgroud)

“预聚合”

雷克斯特: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
Run Code Online (Sandbox Code Playgroud)

返回:

+-------------------------------------+---------------------+---------------------+--------------+
|                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          |
+-------------------------------------+---------------------+---------------------+--------------+
Run Code Online (Sandbox Code Playgroud)

使用 CTE 进行“预聚合”

雷克斯特: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 si


ype*_*eᵀᴹ 6

只是纠正错误假设的注释:

编写枢轴的主要方法有3 种,而不是两种。第三种是使用驱动表和多个连接(使用LEFT JOINOUTER/CROSS APPLY):

这可能或多或少有效,具体取决于几个细节(表分布、索引等)和特定数据透视操作的要求。它与 SUM / GROUP BY 方法有几个不同点:

  • 如果有适当的索引(适当的意思:根据WHERE子句、GROUP BY子句和要聚合的列而不同),则可以避免扫描整个表。在特定示例中,(RateItemTypeID, RateID) INCLUDE (UnitPrice)if上的索引_WherClause_为空。

    • 这在多种情况下是有益的:
      例如,如果表有数百个不同的RateItemTypeID值,但我们的查询只对其中几个感兴趣。扫描整个表(甚至整个索引)与寻找更窄 NCI 的一小部分相比,我希望第二个更有效。
    • 这当然会适得其反,因为它需要为不同的WHERE谓词和聚合列提供不同的索引。
  • GROUP BY乃至整个驱动子查询可以经常由另一表(一取代Rate在该特定示例表)。

  • 在几个枢轴变体上GROUP BY,子查询中的 也可以删除,子查询转换为简单LEFT连接(例如,如果在特定情况下存在UNIQUE约束(RateID, RateItemTypeID))。这表明SUM在“SUM / GROUP BY”方法中(在这些情况下)仅因为GROUP BY对一个值(和几个空值)求和。

查询:

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 ;
Run Code Online (Sandbox Code Playgroud)


The*_*war 5

SQL Server 转换以下查询:

SELECT 
RateID, [1], [2], [3]
FROM PertinentRates
PIVOT (SUM(UnitPrice) FOR RateItemTypeID IN ([1], [2], [3])) PVT)
Run Code Online (Sandbox Code Playgroud)

到:

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
Run Code Online (Sandbox Code Playgroud)

所以选择一个而不是另一个,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)
Run Code Online (Sandbox Code Playgroud)

现在查询

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
Run Code Online (Sandbox Code Playgroud)

现在,当两个查询批量执行时,两者占用相同的成本并且计划大致相同:

在此处输入图片说明

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]))
Run Code Online (Sandbox Code Playgroud)

案件

  |--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]))
Run Code Online (Sandbox Code Playgroud)