【问题标题】:Help improving SQL join帮助改进 SQL 连接
【发布时间】:2011-02-09 14:50:34
【问题描述】:

我有一个存储过程,用于更新用户余额的游戏积分。这是一个带有 5 个子查询的插入。我已将其中一个子查询隔离为减慢整个批处理速度的查询。没有它,存储过程将在 2 秒内运行。有了它,最多需要 8 秒。 8 秒不是世界末日,但为了可扩展性,我需要更快地完成它。这是孤立的子查询:

(SELECT IsNull(Sum(A.TransAmount) + Sum(Case When A.BetResult = 1 Then (A.BetWinAmount + (A.TransAmount * -1)) End), 0)
            FROM User_T A
            LEFT OUTER JOIN User_TD B on A.TID = B.TID
            LEFT OUTER JOIN Lines_BL C ON B.LID = C.LID
            LEFT OUTER JOIN Lines_BM D ON C.BMID = D.BMID
            LEFT OUTER JOIN Event_M E ON D.EID = E.EID
            LEFT OUTER JOIN Event_KB F ON A.TransReason = F.BID
            LEFT OUTER JOIN Event_M G ON F.BID = G.EID
        where A.UserID = U.UserID AND (A.IsSettled = 1)
        AND 
        (
        (A.TransReason = 1 AND (datediff(dd, Convert(datetime, E.EDate, 101), Convert(datetime, @EndDate, 101)) = @DaysAgo)) OR 
        (A.TransReason >= 3000 AND (datediff(dd, Convert(datetime, G.EDate, 101), Convert(datetime, @EndDate, 101)) = @DaysAgo)
                AND  [dbo].[Event_CEAFKBID](A.TransReason) = 1) OR
        (A.TransReason BETWEEN 3 and 150 AND (datediff(dd, Convert(datetime, A.TransDT, 101), Convert(datetime, @EndDate, 101)) = @DaysAgo))
        )

我为进一步隔离所做的工作:当我仅在连接上运行 Select * 时(没有 where 子句),性能非常好 - > 100000 行在一秒钟内。正如我在 where 子句中添加的那样,我相信最大的减速来自 'or' 子句和/或需要评估的函数。

据我了解,where 子句中的函数会评估 每个 行 - 而不是以某种方式缓存函数的定义并以这种方式进行评估。我确实在表上有索引,但我想知道其中一些是否不正确。

如果您不了解完整的数据库结构,我确信很难确定问题出在哪里,但我想指出一个开始进一步隔离的方向。

【问题讨论】:

  • 您查看过查询的执行计划吗?这通常会帮助您指出正确的方向。尤其是在预期可能未使用索引或连接成本最高等情况下。
  • 是的,这是一个功能......什么是消毒?

标签: sql sql-server join indexing


【解决方案1】:

我怀疑您最大的性能损失来自相关子查询(无论 U.UserId 后面的表)和嵌入式函数调用 dbo.Event_CEAFKBID。当然,很大程度上取决于表有多大(正在读取多少行)。所有这些日期时间转换都无济于事,并且会产生非常强烈的“糟糕设计”气味,但我认为它们不会对性能产生太大影响。

那些左外连接很难看,因为优化器必须检查它们的所有行——所以如果“A”很大,所有行上的连接所有必须执行,即使那里没有数据。如果它们可以用内部连接替换,那么就这样做,但我猜不是因为“表 E 表 G”的逻辑。 Lesses,看起来你得到的是三个独立的查询合并为一个;如果你把它分成三个,联合在一起,它看起来就像下面的科学怪人查询。我不知道这是否会运行得更快(哎呀,我什至无法调试查询并确保 pantheses 平衡),但如果你的数据相对于你的逻辑稀疏,它应该运行得很快。 (我去掉了日期转换以使代码更清晰,您必须将它们重新插入。)

SELECT isnull(sum(Total), 0) FinalTotal from (
SELECT 
   sum(A.TransAmount + Case When A.BetResult = 1 Then A.BetWinAmount - A.TransAmount else 0 End) Total
 FROM User_T A            
 INNER JOIN User_TD B on A.TID = B.TID             
INNER JOIN Lines_BL C ON B.LID = C.LID             
INNER JOIN Lines_BM D ON C.BMID = D.BMID             
INNER JOIN Event_M E ON D.EID = E.EID             
 where A.UserID = U.UserID
  AND A.IsSettled = 1
  AND A.TransReason = 1 
  AND (datediff(dd, E.EDate, @EndDate) = @DaysAgo)) 

UNION ALL SELECT 
   sum(A.TransAmount + Case When A.BetResult = 1 Then A.BetWinAmount - A.TransAmount else 0 End) Total
 FROM User_T A            
INNER JOIN Event_KB F ON A.TransReason = F.BID             
INNER JOIN Event_M G ON F.BID = G.EID        
 where A.UserID = U.UserID
  AND A.IsSettled = 1
  AND A.TransReason >= 3000 
  AND (datediff(dd, G.EDate, @EndDate) = @DaysAgo)                 
  AND [dbo].[Event_CEAFKBID](A.TransReason) = 1

UNION ALL SELECT 
   sum(A.TransAmount + Case When A.BetResult = 1 Then A.BetWinAmount - A.TransAmount else 0 End) Total
 FROM User_T A            
 where A.UserID = U.UserID
  AND A.IsSettled = 1
  AND A.TransReason BETWEEN 3 and 150 
  AND datediff(dd, A.TransDT, @EndDate) = @DaysAgo)
) ThreeWayUnion

【讨论】:

  • 太棒了,菲利普!有很多值得思考的地方。我将尝试运行它,看看会发生什么。
  • 做大量的测试。请注意 UNION 和 UNION ALL 之间的关键区别,因为您现在将运行三个相关的子查询而不是一个,它可能会更慢...
  • Phil(或任何人)我正在重新阅读并想知道日期时间转换的替代方案。如果我正在写行,当它们发生在第二个时,我将如何确定行何时落在某个日期?
  • 这是一个棘手且复杂的问题,完全取决于您需要如何处理存储的数据。 (日历日期?距离最后一个日期 24 小时?等等。)查看并熟悉所有日期时间函数(datepart、datediff 等)。当然,您可能真正想要的是dateadd(dd, datediff(dd, 0, @datetime), 0),这是很多 SO 问题的答案,而不是我现在可以快速找到的答案。试试看;唉,它是如何做的超出了评论框的范围。
【解决方案2】:

您可以将 case 放在 where 原因中,而不是直接放在 select 第一行。 如果在此语句中您只使用表 A、E 和 G,为什么需要添加多个连接?

为了更好地执行查询,您可以在 management Studio 上使用执行计划。

【讨论】:

  • 访问表 A、E 和 G 的唯一方法是通过连接中的其他表。我已经使用了执行计划,但破译它将是一个不同的问题。
【解决方案3】:

相关子查询是一种非常糟糕的编程技术,它等同于在查询中使用游标。改为将其设为派生表。

是的,这些功能正在减慢您的速度。如果必须转换为日期时间,则需要修复数据库结构并将数据正确存储为日期时间。

【讨论】:

  • 很棒的信息,谢谢!我将查看派生表,并且我确信我还有许多其他可以清理的存储过程。
  • 您也可以使用 CTE 代替派生表。
【解决方案4】:

您是否需要对DATEDIFF 函数的日期时间进行转换?您是将日期存储为测试,还是重新转换以摆脱时间?如果你是,那么你不需要,因为包括时间在内的不同天数都是正确的。

【讨论】:

  • 好问题 - 真的让我思考......我正在重新转换以摆脱时间。由于发生的不同事件(插入的行),它们有一个实际的时间戳,但我实际上想要一整天的活动。不过,我想我仍然需要重新转换。
  • 如上所述,我删除了日期转换,只看到非常小的性能提升。经过进一步审查,我的转换是多余的,因为作为转换器的数据已经是日期时间数据类型。也许这就是为什么没有可以忽略的性能提升?
【解决方案5】:

您应该检查是否需要外连接 - 它们比内连接更昂贵。您有一些来自主表的值,标记为 A。您还有一个引用 E 的 OR 条件和一个引用 G 的 OR 条件。我希望按照以下方式重组查询:

SELECT SUM(x.result)
  FROM (SELECT A.TransAmount + CASE WHEN A.BetResult = 1
                               THEN (A.BetWinAmount + (A.TransAmount * -1))
                               ELSE 0 END AS result
          FROM A
         WHERE A.TransReason BETWEEN 3 AND 150
           AND datediff(dd, Convert(datetime, A.TransDT, 101),
                            Convert(datetime, @EndDate,  101)) = @DaysAgo
           AND A.UserID = U.UserID    -- Where does alias U come from?
           AND A.IsSettled = 1
        UNION
        SELECT A.TransAmount + CASE WHEN A.BetResult = 1
                               THEN (A.BetWinAmount + (A.TransAmount * -1))
                               ELSE 0 END AS result
          FROM User_T   A
          JOIN User_TD  B ON A.TID  = B.TID
          JOIN Lines_BL C ON B.LID  = C.LID
          JOIN Lines_BM D ON C.BMID = D.BMID
          JOIN Event_M  E ON D.EID  = E.EID
         WHERE A.TransReason = 1
           AND datediff(dd, Convert(datetime, E.EDate,  101),
                            Convert(datetime, @EndDate, 101)) = @DaysAgo
           AND A.UserID = U.UserID    -- Where does alias U come from?
           AND A.IsSettled = 1
        UNION
        SELECT A.TransAmount + CASE WHEN A.BetResult = 1
                               THEN (A.BetWinAmount + (A.TransAmount * -1))
                               ELSE 0 END S result
          FROM User_T   A
          JOIN User_TD  B ON A.TID  = B.TID
          JOIN Lines_BL C ON B.LID  = C.LID
          JOIN Lines_BM D ON C.BMID = D.BMID
          JOIN Event_M  E ON D.EID  = E.EID
          JOIN Event_KB F ON A.TransReason = F.BID
          JOIN Event_M  G ON F.BID  = G.EID
         WHERE A.TransReason >= 3000
           AND datediff(dd, Convert(datetime, G.EDate, 101),
                            Convert(datetime, @EndDate, 101)) = @DaysAgo
           AND [dbo].[Event_CEAFKBID](A.TransReason) = 1
           AND A.UserID = U.UserID    -- Where does alias U come from?
           AND A.IsSettled = 1
       ) AS x

这里的想法是内连接查询每个都比外连接查询快,并且对中间结果求和对 DBMS 来说并不困难(无论如何它都在这样做)。它可能还避免了对IFNULL 的需要。

别名 U 大概是对外部查询的引用,这是它的一部分。

【讨论】:

  • 嗨乔纳森。那里的东西很棒! U 表是用户表。有了您的回复和 Philip 的回复,看来我真的需要了解一下 Union 查询。
  • Jonathan - 我终于有时间坐下来理解这一点了。在您的示例的帮助下,我能够重写子查询的这一部分。我通过分析器运行了新旧脚本,我从 >21,000 次读取和 250 毫秒增加到 1318 次读取和 62 毫秒!希望当我将所有这些放在一起时,我会看到巨大的性能提升!仅供参考,有人建议删除日期转换。我测试了有无和没有只提供了大约 5 毫秒的增益。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多