【问题标题】:How to speed up this TSQL query?如何加快这个 TSQL 查询?
【发布时间】:2018-02-07 06:01:31
【问题描述】:

我有一个运行“慢”的 TSQL 选择查询

SELECT 
    CustomerKey
    ,ProductKey
    ,RepresentativeKey
    ,ReportingDateKey   
    ,SUM(i.InvoiceQuantity) AS InvoiceQuantity
    ,SUM(i.InvoiceQuantityKg) AS InvoiceQuantityKg
    ,SUM(i.BrutoInvoiceLineAmount) AS BrutoInvoiceLineAmount
    ,SUM(i.EndOfYearDiscount) AS EndOfYearDiscount
    ,SUM(i.NettoInvoiceLineAmount) AS NettoInvoiceLineAmount
    ,SUM(i.TotalLineCostPrice) AS CostPrice
    ,SUM(i.MarginAmount) AS MarginAmount

FROM FactInvoices i

WHERE 
    i.DossierKey =2
    AND i.ReportingDate BETWEEN '2016-01-01' AND '2017-12-31'
GROUP BY
    CustomerKey
    ,ProductKey
    ,RepresentativeKey
    ,ReportingDateKey

我正在 SSMS 32 位中运行查询。 执行时间为 17-21s,我测试过在 DossierKey 和 ReportingDate 上添加非聚集索引,但这只会减慢查询速度。

该表有大约 604 万条记录,此结果集返回 10 万条记录。 它在 SQL 2016 开发人员版上运行。 服务器规格:8core 16gb ram 和 HDD => 虚拟服务器。

查看执行计划,我找不到任何改进。 我该如何加快速度?更多硬件?但我认为这不会有帮助,因为在运行此查询时服务器并未完全使用。

编辑: 执行计划:

索引:

CREATE NONCLUSTERED INDEX [_dx1]
ON [dbo].[FactInvoices] ([DossierKey],[ReportingDate])
INCLUDE ([CustomerKey],[ProductKey],[ReportingDateKey],[RepresentativeKey],[InvoiceQuantity],[InvoiceQuantityKg],[BrutoInvoiceLineAmount],[NettoInvoiceLineAmount],[MarginAmount],[EndOfYearDiscount],[TotalLineCostPrice])

谢谢。

【问题讨论】:

  • 您需要为初学者分析 SQL 计划,并调查是否有可以使用的覆盖索引,甚至是过滤索引。一个简单的索引可能不会有太大帮助。
  • 您的意思是查询返回 1M 聚合行吗?考虑到客户端和网络时间,17-21 秒似乎是合理的。
  • 因此,如果结果需要 17 秒,丢弃结果需要 4 秒,那么 SSMS 处理结果集将占用 13 秒。您的申请需要多长时间?
  • @Phoenix 等待您尝试在单个 SSRS 报告中显示 100 万行?在单个报表中显示这么多行肯定会导致 SSRS 大幅减速。另外,为什么一份报告会带来如此大量的数据?您的用户可以合理解析多少信息?
  • @Phoenix 但您自己说查询在 17 秒内运行到 ssms 和 4 秒时丢弃结果。我不认为您的问题是查询运行缓慢。我认为这是您向 SSRS 发送的数据过多。尤其是现在我们知道在 SSMS 中运行一个需要 17 秒的查询需要 60 多秒。

标签: sql sql-server performance tsql sql-server-2016


【解决方案1】:

对于这个查询:

SELECT CustomerKey, ProductKey, RepresentativeKey, ReportingDateKey,
       . . .
FROM FactInvoices i
WHERE i.DossierKey = 2 AND
      i.ReportingDate BETWEEN '2016-01-01' AND '2017-12-31'
GROUP BY CustomerKey, ProductKey, RepresentativeKey, ReportingDateKey;

我会推荐一个关于FactInvoices(DossierKey, ReportingDate, CustomerKey, ProductKey, RepresentativeKey) 的索引。前两个是用于WHERE 子句的索引的主要元素。其余三列可能对聚合有用。您还可以包括查询中使用的所有其他列。

【讨论】:

    【解决方案2】:

    这是我写的一篇关于加速查询的文章。

    如果您的查询速度很慢,您可以检查执行计划以了解可能的加速区域。

    好吧,我已经这样做了,但发现它并不总是有帮助。相同的执行计划可能需要几秒钟才能运行或进入 never never land 并在 7 分钟后被杀死。

    我最近使用了多种技术解决了这个问题,这些技术我以前没有在一个地方提到过,并且想帮助遇到同样情况的其他人。解决方案通常会在 2 秒内返回。

    这就是我所做的。

    开始查询

    这是一个相当基本的查询。它报告销售订单并允许用户指定多达 6 个可选的 where 条件。

    • 如果用户没有输入值的标准,例如 Country,则其标准字符串设置为 '' 并且 Country 不被选中。

    • 如果用户确实为某个值输入了条件,则其条件字符串由'%..%' 括起来。例如,如果用户输入 'Tin',strCountry 将设置为 '%Tin%' 并选择名称中包含 'Tin' 的所有国家/地区。 (例如阿根廷和马提尼克。)

    SELECT Top 1000
        SalesHeader.InvoiceNumber
        ,SalesHeader.CompanyName
        ,SalesHeader.Street
        ,SalesHeader.City
        ,SalesHeader.Region
        ,SalesHeader.Country
        ,SalesHeader.SalesDate
        ,SalesHeader.InvoiceTotal
        ,SalesLineItem.LineItemNbr
        ,SalesLineItem.PartNumber
        ,SalesLineItem.Quantity
        ,SalesLineItem.UnitPrice
        ,SalesLineItem.Quantity * SalesLineItem.UnitPrice    as ExtPrice
        ,PartMaster.UnitWeight
        ,SalesLineItem.Quantity * PartMaster.UnitWeight      as ExtWeight
    FROM dbo.SalesHeader 
    left join dbo.SalesLineItem    on SalesHeader.InvoiceNumber    = SalesLineItem.InvoiceNumber
    left join dbo.PartMaster       on SalesLineItem.PartNumber        = PartMaster.PartNumber
    where
        (@strCountry = ''        or Country like @strCountry)
        and
        (@strCompanyName = ''    or CompanyName like @strCompanyName)
        and
        (@strPartNumber = ''     or SalesLineItem.PartNumber like @strPartNumber)
        and
        (@strInvoiceNumber = ''  or SalesHeader.InvoiceNumber like @strInvoiceNumber)
        and
        (@strRegion = ''         or Region like @strRegion)
        and
        (@mnyExtPrice = 0        or (SalesLineItem.Quantity * SalesLineItem.UnitPrice) > @mnyExtPrice)
    Order By
        InvoiceNumber,
        Region,
        ExtPrice
    

    我是从我工作的数据仓库中获取的。完整查询中有 260,000 条记录。我们将返回的记录限制为 1,000 条,因为用户永远不会想要更多。

    有时查询需要 10 秒或更短的时间,有时我们必须在 7 多分钟后终止它。用户不会等待 7 分钟。

    我们的想法

    有不同的技术可以加快查询速度。以下是我们生成的查询。我将介绍下面使用的每种技术。

    这个新查询通常会在 2 秒或更短的时间内返回结果。

    SELECT 
        InvoiceNumber
        ,Company
        ,Street
        ,City
        ,Region
        ,Country
        ,SalesDate
        ,InvoiceTotal
        ,LineItemNbr
        ,PartNumber
        ,Quantity
        ,UnitPrice
        ,ExtPrice
        ,UnitWeight
        ,ExtWeight
    FROM 
    (
        SELECT top 1000
            IdentityID,
            ROW_NUMBER() OVER (ORDER BY [SalesDate], [Country], [Company], [PartNumber]) as RowNbr
        FROM dbo.SalesCombined with(index(NCI_SalesDt))
        where
            (@strCountry = ''        or Country like @strCountry)
            and
            (@strCompany = ''        or Company like @strCompany)
            and
            (@strPartNumber = ''     or PartNumber like @strPartNumber)
            and
            (@strInvoiceNumber = ''  or InvoiceNumber like @strInvoiceNumber)
            and
            (@strRegion = ''         or Region like @strRegion)
            and
            (@mnyExtPrice = 0        or ExtPrice > @mnyExtPrice)
    ) SubSelect
    Inner Join dbo.SalesCombined on SubSelect.IdentityID = SalesCombined.IdentityID
    Order By
        RowNbr
    

    技术 1 - 非规范化数据。

    我在两个方面很幸运:

    • 数据足够小,可以创建第二个副本。

    • 数据没有经常变化。这意味着我可以构建为查询优化的第二个副本,并允许更新需要一段时间。

    SalesHeader、SalesLineItem 和 PartMaster 表合并到一个 SalesCombined 表中。

    计算的值也存储在 SalesCombined 表中。

    请注意,我将原始表格留在原处。更新这些表的所有代码仍然有效。我必须创建额外的代码,然后将更改传播到 SalesCombined 表。

    技术 2 - 创建一个整数标识值

    这个非规范化表的第一个字段是一个整数标识值。这称为 IdentityID。

    即使我们没有对数据进行非规范化,SalesHeader 中的整数标识值也可以用于它与 SalesLineItem 之间的连接,并加快原始查询速度。

    技术 3 - 在此整数标识值上创建聚集索引

    我在这个 IdentityID 值上创建了一个聚集索引。这是查找记录的最快方法。

    技术 4 - 在排序字段上创建唯一的非聚集索引

    查询的输出按四个字段排序:SalesDate、Country、Company、PartNumber。所以我在 SalesDate、Country、Company 和 PartNumber 这些字段上创建了一个索引。

    然后我将 IdentityID 添加到此索引中。该索引被标记为唯一。这使得 SQL Server 可以尽快从排序字段转到实际记录的地址。

    技巧 5:在非聚集索引中包含所有“Where 子句”字段

    SQL Server 索引可以包含不属于排序的字段。 (谁想到的?这是个好主意。)如果在索引中包含所有 where 子句字段,SQL Server 就不必查找实际记录来获取这些数据。

    这是正常的查找过程: 1)从磁盘读取索引。 2) 转到索引上的第一个条目。 3) 从该条目中找到第一条记录的地址。 4) 从磁盘读取该记录。 5) 查找属于 where 子句的任何字段并应用条件。 6) 确定该记录是否包含在查询中。

    如果在索引中包含 where 子句字段: 1)从磁盘读取索引。 2) 转到索引上的第一个条目。 3) 查找属于 where 子句(存储在索引中)的任何字段并应用条件。 4) 确定该记录是否包含在查询中。

    CREATE UNIQUE NONCLUSTERED INDEX [NCI_InvcNbr] ON [dbo].[SalesCombined]
    (
        [SalesDate] ASC,
        [Country] ASC,
        [CompanyName] ASC,
        [PartNumber] ASC,
        [IdentityID] ASC
    )
    INCLUDE [InvoiceNumber],
        [City],
        [Region],
        [ExtPrice]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
                    IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, 
                    ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
    ON [PRIMARY]
    

    原始查询的执行计划。

    Click Here To See Original Query Execution Plan

    我们最终查询的执行计划要简单得多 - 开始时,它只是读取索引。

    Click Here To See Final Query Execution Plan

    技巧 6:创建子查询来查找要输出的每条记录的 IdentityID 及其排序顺序

    我创建了一个子查询来查找要输出的记录以及输出它们的顺序。请注意以下几点:

    技术 7 - 它明确表示要使用包含所有所需字段的 NCI_InvcNbr 索引。

    技术 8- 它使用 Row_Number 函数为将要输出的每一行生成一个整数。这些值是按照该行 ORDER BY 部分中的字段给出的顺序生成的 1、2 和 3。

    技巧 9:创建包含所有值的封闭查询

    此查询指定要打印的值。它使用 Row_Number 值来了解打印的顺序。请注意,内连接是在 IdentityID 字段上完成的,该字段使用聚集索引来查找要打印的每条记录。

    无济于事的技术

    我们尝试了两种没有加快查询速度的技术。这些语句都添加到查询的末尾。

    • OPTION (MAXDOP 1) 将处理器的数量限制为一个。这将阻止执行任何并行性。我们在试验查询并在执行计划中具有并行性时尝试了这一点。

    • OPTION (RECOMPILE) 导致每次运行查询时都重新创建执行计划。当不同的用户选择可以改变查询结果时,这会很有用。

    希望这个有用。

    【讨论】:

      【解决方案3】:

      如果您已经为此查询建立了索引,但性能仍然很差,您可以尝试按 DossierKey 对表进行分区。

      改变

      WHERE i.DossierKey = 2
      

      WHERE $PARTITION.partition_function_name( 2) 
      

      https://www.cathrinewilhelmsen.net/2015/04/12/table-partitioning-in-sql-server/

      https://docs.microsoft.com/en-us/sql/t-sql/functions/partition-transact-sql

      【讨论】:

      • 表分区并不是真正的性能工具。我绝对不会建立一个分区来帮助 1 个特定的查询。覆盖索引更有可能使特定查询受益。
      猜你喜欢
      • 2019-02-06
      • 2017-12-23
      • 1970-01-01
      • 1970-01-01
      • 2012-12-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-12
      • 1970-01-01
      相关资源
      最近更新 更多