【问题标题】:Optimize SQL query, TSQL优化SQL查询、TSQL
【发布时间】:2015-11-19 17:21:47
【问题描述】:

我是一名软件开发人员,最近 DBA 与我接洽,以优化我的应用程序正在使用的查询。 DBA 报告说,查询在运行时会占用大约 50% 的 CPU 和高 I/O 操作。查询非常简单,我不确定如何优化它。

问题 1:如何优化此查询?

问题 2: 这样做是我的工作吗,DBA 不应该更了解这方面的知识吗?请注意,我们没有 DB 开发人员,只有 DBA 和软件开发人员。

DB 有大约 30-5000 万条记录,它由 DBA 不断维护/监控,但我不确定如何。服务器在专用机器上,是Microsoft SQL Server 2005 - 9.00.5057.00 (X64)

PS:请不要提供通过结构更改来改进 DB 的方法,我知道将货币存储为 varchar 是一个糟糕的设计,但就是这样,我们无法更改 DB结构,仅查询访问它。

感谢您提供任何见解。

查询:

SELECT
    COALESCE(CAST([PH].[PAmount] AS decimal(15, 2)) + CAST([PH].[Fee] AS decimal(15, 2)), 0.0) AS [PayAmount],
    [PH].[PDate] AS [PayDate]
FROM [History] AS [PH] WITH (NOLOCK)
WHERE [PH].[PMode] IN ('C', 'P')
    AND [PH].[INNO] = 'XYZ'
    AND [PH].[PStatus] IN ('CONSERVED', 'EXPECTING', 'REFRIGERATED', 'POSTPONED', 'FILED')
    AND [PH].[Locked] = 1
    AND [PH].[PDate] >= 'Jan 1, 2015'
ORDER BY [PH].[PDate] ASC

字段:

PAmount - 非聚集索引,varchar(50)

Fee - 未编入索引,decimal(6,2)

PDate - 聚集索引,datetime

PMode - 非聚集索引,varchar(5)

INNO - 非聚集索引,varchar(50)

PStatus - 非聚集索引,varchar(50)

Locked - 未编入索引,bit

执行计划: SELECT---Compute Scalar---Filter---NestedLoops-|--Index Seek (Inner Join) | cost 0% Cost 0% Cost 0% Cost 0% | cost 4% |---Key Lookup Cost 96%

【问题讨论】:

  • 表上有什么索引?查询返回多少行?执行计划是什么样的?
  • 缺少一些索引,你可以在这里做很多事情。但是,我强烈建议您停止使用 NOLOCK。这是与金钱打交道的,这个提示意味着你的结果不准确。 blogs.sqlsentry.com/aaronbertrand/bad-habits-nolock-everywhere
  • 仅供参考:Sql Server 2005 不到一年就报废了。
  • @Martin:原帖中提供了索引。我已经编辑了执行路径。请让我知道您还需要什么。谢谢。
  • @Sean:强烈建议我们始终使用 NOLOCK。也许它是我们数据库的一个细节——99% 的数据库事务是插入。我们几乎从不进行编辑。如果在重负载表上并行工作,NOLOCK 可防止记录被锁定。

标签: sql sql-server tsql query-optimization


【解决方案1】:

你是对的,查询看起来很正常。这是一个直截了当的查询,只有“AND”子句,没有“NOT NULL”约束或连接或子选择。条件大多相等(只有日期是相关的)。如果条件中的值(如“C”、“P”、1、“XYZ”、“CONSERVED”等)具有足够的选择性,那么您(或 DBA)应该定义一些索引并且优化器可以使用它.要求 DBA 为表创建适当的索引。

您希望获得多少条结果行?如果有很多(例如 >> 10,000),ORDER BY 子句可能会花费很多。

【讨论】:

  • 结果很小,应该在一百条以下。从表中提取记录的问题很大(20、30、4000 万条记录,我不确切知道,但它有数百万条记录)
【解决方案2】:

我会看看我是否使用 ISNULL 而不是 COALESCE 获得了更好的结果。

另一件事是查看索引。您列出了索引的字段。如果这些字段被多个索引覆盖,我建议为此查询创建一个良好的覆盖索引。

覆盖索引是查询所需的所有数据都包含在索引中的索引。如果查询使用的索引未覆盖,则需要对表进行额外的旅行(或旅行)以获取其余字段。如果所有数据都在查询中,效率会更高。

查看这些文章:

What are Covering Indexes and Covered Queries in SQL Server?

https://www.simple-talk.com/sql/learn-sql-server/using-covering-indexes-to-improve-query-performance/

对于不属于联接或 where 子句的数据,您可以使用 include 关键字。包含的字段不是索引的可搜索部分,但会保存到数据库的行程。

试试下面的索引。 where 子句中的所有字段都是索引的可搜索部分的一部分,并且包含不属于 where 子句的所有返回字段。在查看执行计划后,您可能需要按顺序进行操作,但我猜对了。

Create Nonclustered Index Ix_Ncl_History_CoveringBigSelect on History(PDate, PMode, INNO, PStatus, Locked) Include (PAmount, Fee)

这是一篇关于包含列的文章。

【讨论】:

  • 没有改进 如果我完全删除 COALESCE(不检查 ISNULL,直接删除 COALESCE)。每个索引都是一个独立的,表中还有其他字段和其他索引,彼此独立。我不完全确定你的意思one index or manymight try making one good covering index for this query,你能详细说明一下吗?
  • 您回答了我关于一个或多个索引的问题。我在问您列出的索引字段是否都在一个索引中,或者是否有几个不同的索引。我将使用有关覆盖索引的更多信息来编辑我的答案。
  • 谢谢,您的索引与 Joel 的索引几乎相同。当我用 10K 数据测试他的索引时,它似乎工作得更好,但是当我注入更多数据时,查询又回到了与原来相同的性能。我不知道为什么。我的测试用例有问题吗?
  • 看看乔斯,我看到的最大不同是他有被收录的状态,而且我把它放在索引中。我会试试的。然后,按照 Joel 的建议,使用索引中的顺序。查看不同配置的执行计划。
【解决方案3】:

您似乎对索引有误解。索引不会相互结合,因此这不是“索引”或“未索引”列的问题。为各个列设置单独的索引并不好。它是关于具有多个列的索引,这与单个查询有关。如果数据库先在另一列上选择仍然更有效,那么列上的索引将无助于查询。

我对此有点陈旧,但对于这个查询,我建议使用如下所示的索引:

CREATE NONCLUSTERED INDEX [ix_History_XXXXX] ON [History] 
(
    [INNO] ASC,
    [Locked] ASC,
    [PDate] ASC,
    [PMode] ASC
)
INCLUDE ( PStatus, PAmount, Fee)

您可能希望交换 PDate、PMode 和 PStatus,具体取决于它们的 selectivity

在构建索引时,您希望首先列出最具体的项目。一般的想法是索引按顺序存储每个连续的项目。使用此索引,INNO 的所有 XYZ 值的行将组合在一起,因此查询引擎可以搜索 到索引的该部分。下一个最具体的列是Locked。即使这是一个bit 值,因为它仅限于一个值,我们仍然能够搜索 直接找到对整个查询很重要的索引的一个特定部分。再说一遍:我已经有一段时间不用做这种事情了,所以你也可以在这里列出PMode;我只是不记得 Sql Server 查询优化器是否足够聪明,可以有效地处理这两个值。

从这里开始,索引的最佳选择取决于每个查询值对结果的限制程度。由于我们不再能够将所有结果放在一个空间中,我们将不得不扫描索引的相关部分。我的直觉是接下来使用Date 值。这将允许扫描从与您的结果匹配的第一个日期开始遍历索引,并帮助它以正确的顺序获取记录,但同样:这只是我的直觉。先列出 PMode 或 PStatus 可能会做得更好。

最后,INCLUDES 子句中的附加项将允许您完全从索引完成此查询,而无需实际返回完整表。您使用 INCLUDES 子句而不是仅将值附加到查询以避免使 Sql Server 重建索引以更新这些列。这就是为什么 PStatus 可能不应该是主索引的一部分,如果状态是可以改变的,以及为什么你可能最好也将Locked 排除在索引之外.不过,这些都是您想要衡量并自己测试的。

【讨论】:

  • 我不确定我是否理解您所写的有关索引的一半内容。据此判断,我的 SQL 充其量是平庸的。我不是设计索引的人,我不确定是谁设计的,可能当时整个系统都很小,没有人打扰或知识渊博,无法正确完成。我不能直接更改生产中的索引,但如果我能证明它在测试中工作,我可以建议更改它。我将尝试在 TEST 中应用您的索引 - 我将复制表,因此我有 2 个完全相同的表具有相同的数据,然后我将您的索引应用于其中一个表并比较执行路径。
  • 我创建了 2 个结构完全相同但没有数据的表。一张桌子有额外的索引 - 你的。执行计划表明没有区别。我真的需要其中的数据吗?我试图生成脚本来复制带有数据的表,但它出错了,说它太大或类似的东西。
  • 对没有数据的单个表的查询永远不会使用索引。
  • 好的,无法将数据复制到每个表...这可能需要一些时间
  • 您不需要所有 5000 万条记录。足以让索引产生影响。几千人应该这样做。
【解决方案4】:

我会简单地在下表上创建索引:

CREATE NONCLUSTERED INDEX idx_History_Locked_PMode_INNO_PStatus_PDate_iPAmount_iFee
    ON dbo.History (Locked, PMode, INNO, PStatus, PDate)
    INCLUDE (PAmount, Fee)
WHERE Locked = 1;                -- This is optional, can reduce index size.

这应该会改善您当前的查询。此处应满足所有条件。

【讨论】:

  • 索引内的顺序很重要。首先列出Locked 似乎很奇怪,因为我猜测特定记录会定期锁定和解锁。有点,它的选择性也很差。最好先列出更具体、更稳定的项目。
  • 好吧,我不喜欢猜测,但我认为如果行被锁定一次,它就会被锁定一次并且永远。我可能错了。不幸的是,当没有示例数据时,很难判断哪种索引最适合。我同意您的索引可能会更好。也许在这里添加过滤器会更好:WHERE Locked = 1。这应该会减少索引大小。谢谢。
  • 锁定的bit更多的是表示相关记录链中最后更新的记录。想象一下从PLANTED 状态到SEEDLINGPLANTHARVESTED 的apple12345 记录。每次 apple12345 的记录进入下一个阶段时,DB 触发器都会将 LOCKED = 1 设置为最新记录,同时将其设置为 0 以设置之前的任何相关记录。
【解决方案5】:

正如您所说,我假设您对数据库无能为力,包括索引和结构更改。那么客户端App环境呢,是否强大到可以做客户端计算呢?

如果答案是肯定的,我建议将计算移到客户端:

  • 不要在查询中转换数据类型,将varchar转换为十进制消耗 CPU 资源。所以直接得到结果并在 你的应用。
  • 对于 IO 问题,请尝试删除 IN 条件,因为 IN 本质上是 “或”条件。因此,使用“=”将您的查询分成小块 条件并发送到您的应用,使用您的客户端应用“联合”它们。

【讨论】:

  • 不强制转换似乎没有什么不同。将 IN 变成一堆 OR 也是如此。我可以对客户端进行计算,但单独拉取字段并不能改善查询。将它们保留为 varchar 而不是强制转换也不会改善它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-10
  • 2014-02-04
  • 2014-11-16
  • 2011-06-22
相关资源
最近更新 更多