【问题标题】:CTE very slow when Joined加入时 CTE 非常慢
【发布时间】:2014-05-13 11:39:54
【问题描述】:

我之前发布过类似的内容,但我现在从不同的方向来处理这个问题,所以我提出了一个新问题。我希望这没问题。

我一直在与 CTE 合作,该 CTE 根据父费用创建费用总和。 SQL和细节可以在这里看到:

CTE Index recommendations on multiple keyed table

我认为我在 CTE 上没有遗漏任何内容,但是当我将它与大数据表(350 万行)一起使用时遇到了问题。

tblChargeShare 包含我需要的一些其他信息,例如InvoiceID,因此我将我的CTE 放在视图vwChargeShareSubCharges 中并将其加入到表中。

查询:

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

在几毫秒内返回结果。

查询:

Select ChargeID from tblChargeShare Where InvoiceID = 1045854

返回 1 行:

1291094

但是查询:

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

运行需要 2-3 分钟。

我保存了执行计划并将它们加载到 SQL Sentry 中。快速查询的树如下所示:

慢查询的计划是:

我尝试过重新索引、通过优化顾问和各种子查询组合运行查询。只要连接包含 PK 以外的任何内容,查询就会很慢。

我在这里也有类似的问题:

SQL Server Query time out depending on Where Clause

它使用函数对子行进行求和,而不是 CTE。这是使用 CTE 进行的重写,以尝试避免我现在遇到的相同问题。我已经阅读了该答案中的回复,但我并不聪明 - 我阅读了一些关于提示和参数的信息,但我无法让它发挥作用。我曾认为使用 CTE 重写会解决我的问题。在几千行的 tblCharge 上运行时,查询速度很快。

在 SQL 2008 R2 和 SQL 2012 中测试

编辑:

我已将查询压缩为一条语句,但同样的问题仍然存在:

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

UNION ALL

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

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

编辑: 多玩玩,我就是不明白。

这个查询是即时的(2ms):

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

而这需要 3 分钟:

DECLARE @ChargeID int = 1291094

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

即使我将成堆的数字放在“In”中,查询仍然是即时的:

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

编辑 2:

我可以使用这个示例数据从头开始复制:

我创建了一些虚拟数据来复制问题。它不是那么重要,因为我只添加了 100,000 行,但是仍然会发生错误的执行计划(在 SQLCMD 模式下运行):

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

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

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

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

UNION ALL

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

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

然后运行这两个查询:

--Slow Query:
Declare @ChargeID int = 60900

Select *
from [vwChargeShareSubCharges]
Where MasterChargeID = @ChargeID

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

【问题讨论】:

  • 只是一个快速的想法...如果您使用查询Select t.* from vwChargeShareSubCharges t,将视图vwChargeShareSubCharges 替换为其实际的tsql 定义,酌情加入其他表,然后运行查询,是更快吗?
  • 完全一样,如果我粘贴完整的 CTE。

标签: sql sql-server-2008-r2 common-table-expression


【解决方案1】:

在这里,SQL Server 可以为您做的最好的事情是将ChargeID 上的过滤器向下推到视图内递归 CTE 的锚点部分。这允许寻求找到构建层次结构所需的唯一行。当您将参数作为常量值提供时,SQL Server 可以进行优化(使用名为 SelOnIterator 的规则,适用于那些对这类事情感兴趣的人):

当您使用局部变量时,它无法做到这一点,因此ChargeID 上的谓词卡在视图之外(从所有NULL id 开始构建完整的层次结构):

在使用变量时获得最佳计划的一种方法是强制优化器在每次执行时编译一个新计划。然后在执行时根据变量中的特定值调整生成的计划。这是通过添加OPTION (RECOMPILE) 查询提示来实现的:

Declare @ChargeID int = 60900;

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

第二种选择是将视图更改为内联表函数。这允许您明确指定过滤谓词的位置:

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

  UNION ALL

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

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

像这样使用它:

Declare @ChargeID int = 60900

select *
from dbo.udfChargeShareSubCharges(@ChargeID)

查询还可以从ParentChargeID 上的索引中受益。

create index ix_ParentChargeID on tblChargeTest(ParentChargeID)

这是关于类似场景中类似优化规则的另一个答案。 Optimizing Execution Plans for Parameterized T-SQL Queries Containing Window Functions

【讨论】:

  • 谢谢。我今天会玩这些,看看我能取得什么成就。感谢您的详细回答。
  • OPTION (RECOMPILE) 非常适合 @ChargeID 变量,但如果我以任何方式加入 CTE,仍然会非常缓慢。我已将 InvoiceID 键添加到接地表并按照此处的建议创建了一个 TABLE 函数,它既好又快。
【解决方案2】:

接下来要找到解决方案,我建议将 CTE 选择到临时表中并从那里加入。根据加入 CTE 的个人经验,我的查询返回了 5 分钟,而只需将 CTE 生成的数据插入到临时表中,它就可以缩短到 4 秒。我实际上是将两个 CTE 连接在一起,但我想当 CTE 连接到 LONG 表(尤其是外部连接)时,这将适用于所有长时间运行的查询。


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

    ;WITH cte as ( 
    select * from t1
    )

    select * 
    into #p
    from cte

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

【讨论】:

  • 非常感谢您的建议。我有一个加入两个 CTE 的查询,我看了一段时间,看不到任何明显的问题。当我把其中一个做成临时表时,时间从 3 多分钟减少到 2 秒。我只能对 MS 说的是(HSJ/MoG),也许它在 klugey 方面,但如果它带来了约 2 个数量级的性能改进,为什么不在内部也以这种方式实现它???
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-12
  • 1970-01-01
  • 2020-05-11
  • 2021-03-01
相关资源
最近更新 更多