如何加快这个TSQL查询?

时间:2017-08-29 17:20:11

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

我有一个正在运行的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 32bit中运行查询。 执行时间是17-21s,我已经测试过在DossierKey和ReportingDate上添加非聚集索引,但这只会减慢查询速度。

该表有大约6.04M的记录,该结果集返回1M记录。 它在SQL 2016开发人员版上运行。 服务器规格:8核16gb RAM和HDD =>虚拟服务器。

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

编辑: 执行计划: enter image description here

指数:

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

感谢。

3 个答案:

答案 0 :(得分:3)

对于此查询:

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子句的索引的主要元素。其余三列可用于聚合。您还可以包含查询中使用的所有其他列。

答案 1 :(得分:1)

这是我写的关于加快查询速度的文章。

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

好吧,我这样做了,发现它并不总是有帮助。相同的执行计划可能需要花费几秒钟来运行,否则可能要花7分钟才能被杀死。

最近,我使用以前从未在一个地方提到过的各种技术解决了这个问题,并希望在相同情况下为其他任何人提供帮助。该解决方案通常在2秒内返回。

这就是我所做的。

开始查询

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

•如果用户未输入值的条件(例如,国家/地区),则其条件字符串设置为”,并且未选中国家/地区。

•如果用户确实输入了一个值的条件,则其条件字符串将用'%..%'括起来。例如,如果用户输入“ 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)使每次运行查询时都重新创建执行计划。当不同的用户选择可以改变查询结果时,这很有用。

希望这很有用。

感谢科恩·费里(Korn Ferry)数据库系统高级总监Lane Sandness的帮助。

答案 2 :(得分:-1)

如果您已经将此查询编入索引并且性能仍然不佳,则可以尝试使用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