【问题标题】:How to improve this TSQL Statement with CTE如何使用 CTE 改进此 TSQL 语句
【发布时间】:2020-01-13 21:27:11
【问题描述】:

我在处理一个查询时遇到了一些麻烦。它利用了子查询和附加连接,我确信有一种方法可以更好、更易读、更高效地构建它,可能使用 CTE,但我不确定如何。

这里是:我有一个包含库存历史的表和另一个包含项目价格历史的表。我需要使用当时的最新价格计算每个历史条目的库存价值。

为了这篇文章的目的,我稍微简化了表格,只使用了一个库存中的一个项目。库存表如下所示:

select * 
from Temp.dbo.InvHist 
order by Date

历史价格如下所示:

select *
from Temp.dbo.PriceHist
order by Date

为了获得每个库存日期的最新价格,我首先需要从价格表中获取正确的日期:

select 
    InvHist.Date
    ,InvHist.Item
    ,InvHist.Amount
    ,max(PriceHist1.Date) DatePrice
from Temp.dbo.InvHist 
left join (
    select PriceHist.Item, PriceHist.Date
    from Temp.dbo.PriceHist
    group by PriceHist.Item, PriceHist.Date
) PriceHist1 on InvHist.Item = PriceHist1.Item and PriceHist1.Date <= InvHist.Date
group by 
    InvHist.Date
    ,InvHist.Item
    ,InvHist.Amount
order by
    InvHist.Date

最后我再次加入价格表,以获得可以计算股票价值的正确价格:

select 
    a.*
    ,b.Price
    ,a.Amount * b.Price InvValue
from (
    select 
        InvHist.Date
        ,InvHist.Item
        ,InvHist.Amount
        ,max(PriceHist1.Date) DatePrice
    from Temp.dbo.InvHist 
    left join (
        select PriceHist.Item, PriceHist.Date
        from Temp.dbo.PriceHist
        group by PriceHist.Item, PriceHist.Date
    ) PriceHist1 on InvHist.Item = PriceHist1.Item and PriceHist1.Date <= InvHist.Date
    group by 
        InvHist.Date
        ,InvHist.Item
        ,InvHist.Amount
) a
left join Temp.dbo.PriceHist b on a.Item = b.Item and a.DatePrice = b.Date

那么,有没有人知道如何以更高效、更优雅的方式获得这个结果?

【问题讨论】:

  • 您的查询看起来不错。只有当您有一个多次使用的公共子查询或需要使用您的查询不使用也不需要的自引用查询(例如用于递归)时,CTE 才真正有用。对内部查询执行连接非常好,并且表明正确使用 SQL。
  • 如果您的性能很差,这意味着您缺少索引或您的 STATISTICS 对象已过期。您是否查看过查询执行计划?他们会告诉你为什么查询很慢。
  • @Dai 是正确的。但是,我倾向于将事物从子查询中移出并放入 CTE,因为它使我更容易测试以确保我实际上选择了我想要的东西。我也更容易考虑何时将其构建在 CTE 中,但我个人不知道性能差异是什么。
  • 旁白:首先,避免使用premature optimization(参见performance rant。)。如果存在性能问题,请查看实际执行计划以找到瓶颈。
  • CTE 在很大程度上是“语法糖”,不会影响性能。在绝大多数情况下,将子选择转换为 CTE 后,查询计划将完全相同。如果您尝试优化查询的性能,CTE 不是您的答案。如果您正在针对可读性、可维护性和可测试性进行优化,那么 CTE 非常棒。

标签: sql-server tsql common-table-expression inventory-management


【解决方案1】:

早安,

由于您没有提供 DDL+DML,我无法测试查询。而且,任何关于性能的讨论都是毫无意义的。

以下查询未经测试,仅介绍了在您的特定情况下使用 CTE 而不是子查询的基本思想。此答案仅出于学习目的而提供,而不是出于任何生产原因。

将主子查询转换为一个 CTE:

如您所见,我只是将子查询的内容复制到 CTE 定义中。这很简单

;With MyCTE as(
    select 
        InvHist.Date
        ,InvHist.Item
        ,InvHist.Amount
        ,max(PriceHist1.Date) DatePrice
    from Temp.dbo.InvHist 
    left join (
        select PriceHist.Item, PriceHist.Date
        from Temp.dbo.PriceHist
        group by PriceHist.Item, PriceHist.Date
    ) PriceHist1 on InvHist.Item = PriceHist1.Item and PriceHist1.Date <= InvHist.Date
    group by 
        InvHist.Date
        ,InvHist.Item
        ,InvHist.Amount
)
select a.*,b.Price,a.Amount * b.Price InvValue
from MyCTE a
left join Temp.dbo.PriceHist b on a.Item = b.Item and a.DatePrice = b.Date
GO

现在,让我们更上一层楼,将主子查询中的子查询分解为单独的 CTE

这一步有点复杂(我不能确定我没有犯语法错误,因为我无法测试它)。

;With MyCTE01 as(
    select 
        InvHist.Date
        ,InvHist.Item
        ,InvHist.Amount
        ,max(PriceHist1.Date) DatePrice
    from Temp.dbo.InvHist 
),
MyCTE02 as (
    select PriceHist.Item, PriceHist.Date
    from Temp.dbo.PriceHist
    group by PriceHist.Item, PriceHist.Date
),
MyCTE03 as (
    select Date,Item,Amount,DatePrice
    from MyCTE01
    left join MyCTE02 on MyCTE01.Item = MyCTE02.Item and MyCTE02.Date <= MyCTE01.Date
    group by MyCTE01.Date,MyCTE01.Item,MyCTE01.Amount
)
select a.*,b.Price,a.Amount * b.Price InvValue
from MyCTE03 a
left join MyCTE02 b on a.Item = b.Item and a.DatePrice = b.Date

【讨论】:

  • 感谢您的关注,这绝对是学习的好去处。 CTE 确实使查询更具可读性,但在这种情况下不会提高性能。
【解决方案2】:

我使用交叉应用将其缩小,以获取给定日期的正确价格。它肯定会缩减您的执行计划,但它肯定会从一些索引中受益。

注意表别名,我使用了临时表,所以我只是为相同的名称加上别名,以便与您的原始查询匹配:

CREATE TABLE #InvHist ([date] date, Item varchar(20),Amount int)

INSERT INTO #InvHist VALUES('01-31-16','NDS.09011012',12)
INSERT INTO #InvHist VALUES('02-29-16','NDS.09011012',11)
INSERT INTO #InvHist VALUES('03-31-16','NDS.09011012',8)
INSERT INTO #InvHist VALUES('04-30-16','NDS.09011012',6)
INSERT INTO #InvHist VALUES('05-31-16','NDS.09011012',6)
INSERT INTO #InvHist VALUES('06-30-16','NDS.09011012',32)
INSERT INTO #InvHist VALUES('07-31-16','NDS.09011012',32)
INSERT INTO #InvHist VALUES('08-31-16','NDS.09011012',28)
INSERT INTO #InvHist VALUES('09-30-16','NDS.09011012',26)
INSERT INTO #InvHist VALUES('10-31-16','NDS.09011012',26)
INSERT INTO #InvHist VALUES('11-30-16','NDS.09011012',23)
INSERT INTO #InvHist VALUES('12-31-16','NDS.09011012',21)


CREATE TABLE #PriceHist([date] date, Item varchar(20), Price int)
INSERT INTO #PriceHist VALUES('07-26-06','NDS.09011012',93894)
INSERT INTO #PriceHist VALUES('10-25-06','NDS.09011012',98119)
INSERT INTO #PriceHist VALUES('04-26-07','NDS.09011012',102828)
INSERT INTO #PriceHist VALUES('06-23-07','NDS.09011012',102599)
INSERT INTO #PriceHist VALUES('05-27-08','NDS.09011012',10701)
INSERT INTO #PriceHist VALUES('05-26-09','NDS.09011012',89649)
INSERT INTO #PriceHist VALUES('10-20-10','NDS.09011012',90783)
INSERT INTO #PriceHist VALUES('01-26-12','NDS.09011012',89991)
INSERT INTO #PriceHist VALUES('05-24-14','NDS.09011012',131496)
INSERT INTO #PriceHist VALUES('03-28-15','NDS.09011012',141873)
INSERT INTO #PriceHist VALUES('05-14-16','NDS.09011012',149738)
INSERT INTO #PriceHist VALUES('06-25-16','NDS.09011012',15318)
INSERT INTO #PriceHist VALUES('03-25-17','NDS.09011012',15459)
INSERT INTO #PriceHist VALUES('10-21-17','NDS.09011012',156352)
INSERT INTO #PriceHist VALUES('03-30-18','NDS.09011012',154869)
INSERT INTO #PriceHist VALUES('03-29-19','NDS.09011012',155154)


    SELECT 
        InvHist.Date
        ,InvHist.Item
        ,InvHist.Amount
        ,PH.PriceDate
        ,PH.Price
        ,InvHist.Amount * PH.Price InvValue
        FROM #InvHist InvHist
    CROSS APPLY (SELECT TOP(1) MAX(date) PriceDate, Price 
            FROM #PriceHist PriceHist 
            WHERE InvHist.Item = PriceHist.Item and PriceHist.date <=  InvHist.date
            GROUP BY Price ORDER by PriceDate desc) PH

【讨论】:

  • 感谢您的建议。我实际上更喜欢您的可读性解决方案,但是有趣的是,应用这种方法比较慢。我会在答案中解释
【解决方案3】:

因此,真正的表是(在德语中)LagerbestandHistorie 有 360 万行和 HerstellkostenHistorie 有 6190 万行,日期都从 2006 年到 2020 年。我为它们都添加了一个聚集列存储索引。

我只比较了两个查询,一个是子查询,一个是交叉应用。

第一个看起来像这样:

select 
    a.*
    ,b.HerstellkostenkomponenteNr
    ,b.Betrag
from (
    select 
        lbh.Datum
        ,lbh.ArtikelNr
        ,lbh.LagerNr
        ,lbh.LagerplatzNr
        ,lbh.Menge
        ,max(hkh1.Datum) PreisDatum
    from LagerbestandHistorie lbh 
    left join (
        select hkh.Datum, hkh.ArtikelNr
        from HerstellkostenHistorie hkh
        group by hkh.Datum, hkh.ArtikelNr
    ) hkh1 on lbh.ArtikelNr = hkh1.ArtikelNr and hkh1.Datum <= lbh.Datum
    group by 
        lbh.Datum
        ,lbh.ArtikelNr
        ,lbh.LagerNr
        ,lbh.LagerplatzNr
        ,lbh.Menge
) a
left join HerstellkostenHistorie b on a.ArtikelNr = b.ArtikelNr and a.PreisDatum = b.Datum
order by
    a.Datum
    ,a.ArtikelNr
    ,a.LagerNr
    ,a.LagerplatzNr
    ,b.HerstellkostenkomponenteNr

它在 26 分钟内返回了结果(1200 万行)。

另一个是这样的:

select 
    lbh.Datum
    ,lbh.ArtikelNr
    ,lbh.LagerNr
    ,lbh.LagerplatzNr
    ,lbh.Menge
    ,hk1.PreisDatum
    ,hkh2.HerstellkostenkomponenteNr
    ,hkh2.Betrag
from LagerbestandHistorie lbh
cross apply (
    select 
        max(Datum) PreisDatum
    from HerstellkostenHistorie hkh 
    where 
        lbh.ArtikelNr = hkh.ArtikelNr and hkh.Datum <= lbh.Datum
    ) hkh1
left join HerstellkostenHistorie hkh2 on lbh.ArtikelNr = hkh2.ArtikelNr and hkh1.PreisDatum = hkh2.Datum
order by
    lbh.Datum
    ,lbh.ArtikelNr
    ,lbh.LagerNr
    ,lbh.LagerplatzNr
    ,hk2.HerstellkostenkomponenteNr

完成需要 44 分钟!

执行计划如下所示:

现在我将坚持第一个查询。感谢您的所有回复。

【讨论】:

  • 您的交叉申请中没有建议的 TOP(1)。当然还有订单。
  • 没错,我必须这样做,因为真正的 HerstellkostenHistorie 表每件商品有多个价格组成部分。
【解决方案4】:

尝试用临时表#temp替换内左连接查询

left join (
        select PriceHist.Item, PriceHist.Date
        from Temp.dbo.PriceHist
        group by PriceHist.Item, PriceHist.Date
    ) PriceHist1 on InvHist.Item = PriceHist1.Item and PriceHist1.Date <= InvHist.Date

select PriceHist.Item, PriceHist.Date
         INTO #temp
        from Temp.dbo.PriceHist
        group by PriceHist.Item, PriceHist.Date

然后在left join中使用#temp,你需要用Set Statistics IOprofiler检查这个查询的逻辑读取在哪里逻辑读取很重。

【讨论】:

  • 为什么临时表会改进查询?
  • 为了更好的 sql 计划尽量避免复杂的查询。
猜你喜欢
  • 1970-01-01
  • 2022-08-03
  • 2020-11-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-01
  • 2016-06-20
相关资源
最近更新 更多