【问题标题】:Optimizing ORDER BY优化 ORDER BY
【发布时间】:2017-07-30 15:12:34
【问题描述】:

我正在尝试优化这个查询,它按reputation 字段(第一个)和id 字段(第二个)对posts 进行排序。如果没有第一个字段查询需要 ~0.250 秒,但它需要高达 ~2.500 秒(意味着慢 10 倍,可怕)。有什么建议吗?

SELECT -- everything is ok here
FROM posts AS p
ORDER BY 
    -- 1st: sort by reputation if exists (1 reputation = 1 day)
    (CASE WHEN p.created_at >= unix_timestamp(now() - INTERVAL p.reputation DAY) 
        THEN +p.reputation ELSE NULL END) DESC, -- also used 0 instead of NULL
    -- 2nd: sort by id dec
    p.id DESC
WHERE p.status = 'published' -- the only thing for filter
LIMIT 0,10 -- limit provided as well

注意事项:
- 使用 InnoDB (MySQL 5.7.19)
- 主要是idposts
- 字段同时被 created_atreputation 编入索引

解释结果:

# id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra # '1', 'SIMPLE', 'p', NULL, 'ALL', NULL, NULL, NULL, NULL, '31968', '100.00', '使用文件排序'

更新^^

声誉规定:一个帖子,多少天(n=声誉)可以显示在列表的顶部。

实际上,我试图为一些可以在列表顶部获取的帖子提供声誉,并找到解决方案:Order posts by "rep" but only for "one" day limit。但经过一段时间(大约 2 年)后,由于表数据量的增加,该解决方案现在变成了一个问题。如果我不能解决这个问题,那么我应该从服务中删除该功能。

更新^^

-- all date's are unix timestamp (bigint)
SELECT p.*
    , u.name user_name, u.status user_status
    , c.name city_name, t.name town_name, d.name dist_name
    , pm.meta_name, pm.meta_email, pm.meta_phone
    -- gets last comment as json
    , (SELECT concat("{", 
        '"id":"', pc.id, '",', 
        '"content":"', replace(pc.content, '"', '\\"'), '",', 
        '"date":"', pc.date, '",', 
        '"user_id":"', pcu.id, '",', 
        '"user_name":"', pcu.name, '"}"') last_comment_json 
        FROM post_comments pc 
        LEFT JOIN users pcu ON (pcu.id = pc.user_id) 
        WHERE pc.post_id = p.id
        ORDER BY pc.id DESC LIMIT 1) AS last_comment
FROM posts p
    -- no issues with these
    LEFT JOIN users u ON (u.id = p.user_id)
    LEFT JOIN citys c ON (c.id = p.city_id)
    LEFT JOIN towns t ON (t.id = p.town_id)
    LEFT JOIN dists d ON (d.id = p.dist_id)
    LEFT JOIN post_metas pm ON (pm.post_id = p.id)
WHERE p.status = 'published'
GROUP BY p.id
ORDER BY 
    -- everything okay until here
    -- any other indexed fields makes query slow, not just "case" part
    (CASE WHEN p.created_at >= unix_timestamp(now() - INTERVAL p.reputation DAY) 
        THEN +p.reputation ELSE NULL END) DESC, 
    -- only id field (primary) is effective, no other indexes 
    p.id DESC
LIMIT 0,10;

解释;

# id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra 1, PRIMARY, p, , ref, PRIMARY,user_id,status,reputation,created_at,city_id-town_id-dist_id,title-content, status, 1, const, 15283, 100.00, 使用索引条件;使用临时的;使用文件排序 # 不知道,这些连接没有使用,但是如果我从选择部分中删除返回字段显示“使用索引条件” 1, PRIMARY, u, , eq_ref, PRIMARY, PRIMARY, 2, p.user_id, 1, 100.00, 1, PRIMARY, c, , eq_ref, PRIMARY, PRIMARY, 1, p.city_id, 1, 100.00, 1, PRIMARY, t, , eq_ref, PRIMARY, PRIMARY, 2, p.town_id, 1, 100.00, 1, PRIMARY, d, , eq_ref, PRIMARY, PRIMARY, 2, p.dist_id, 1, 100.00, 1, PRIMARY, pp, , eq_ref, PRIMARY, PRIMARY, 2, p.id, 1, 100.00, 2, DEPENDENT SUBQUERY, pc, , ref, post_id,visibility,status, post_id, 2, func, 2, 67.11, 使用索引条件;使用哪里;使用文件排序 2, 相关子查询, pcu, , eq_ref, PRIMARY, PRIMARY, 2, pc.user_id, 1, 100.00,

【问题讨论】:

  • 没有索引可以用于 CASE 语句的排序。你能解释一下那个 CASE 语句的逻辑吗?
  • 嗨@Paul,感谢您的回复。我已经更新了我的问题。
  • 虽然在集合论方面不是一个完美的解决方案,但您可以每天运行一次设置声誉的程序/事件(如果您想保持原始声誉,则可以复制该字段)当其生命周期结束时为 0。您还可以找到可以使用索引建模的类似排序,但是对于您的特定公式(“生命周期”结束时的硬截止并切换到 id;并且代表 100 的帖子将始终在具有代表的帖子之前排序99,即使 rep100-post 已经显示了过去 90 天),我没有看到(至少乍一看)一个可索引的公式。
  • 嘿,我想到了,使用一个类似cron的工具。但似乎问题与不同的东西有关。因为,即使 order by 子句中的每个字段都已经被索引,当我给第二个字段排序时,应用程序会变慢。想不通。
  • 如果id 是您的主键,并且您的where 子句中没有任何内容,则order by newreputationcolumn desc, id desc 应该可以正常工作。否则,您将需要(newreputationcolumn, id) 上的复合索引。如果这不起作用,请添加解释输出。重要的是您不要按派生值排序(如果您只是使用事件/cronjob 在其中写“0”而不是动态计算它,则不会这样做)。

标签: mysql sql-order-by query-optimization


【解决方案1】:

这是一个非常有趣的查询。在其优化过程中,您可能会发现并了解许多有关 MySQL 工作原理的新信息。我不确定我是否有时间一次将所有细节都写出来,但我可以逐步更新。

为什么慢

基本上有两种情况:

快速 场景中,您正在以某种预定义的顺序遍历一个表,并且可能同时通过 id 从其他表的每一行快速获取一些数据。在这种情况下,只要您的 LIMIT 子句指定了足够的行,您就会停止行走。订单从哪里来?来自表上的 b 树索引或子查询中结果集的顺序。

slow 场景中,您没有预定义的顺序,MySQL 必须将所有数据隐式放入临时表中,根据某个字段对表进行排序并返回 n em> 来自您的 LIMIT 子句的行。如果您放入该临时表的任何字段是 TEXT 类型(不是 VARCHAR),MySQL 甚至不会尝试将该表保留在 RAM 中,而是在磁盘上刷新和排序(因此需要额外的 IO 处理)。

首先要修复

在许多情况下,您无法构建允许您遵循其顺序的索引(例如,当您从不同表中对列进行 ORDER BY 时),因此在这种情况下的经验法则是尽量减少MySQL 将放入临时表。你怎么能这样做?您只选择子查询中行的标识符,并在获得 id 后,将 id 连接到表本身和其他表以获取内容。那就是你用一个订单制作一个小桌子,然后使用快速场景。 (这与一般的 SQL 略有矛盾,但每种 SQL 都有自己的方法来优化查询)。

巧合的是,您的SELECT -- everything is ok here 看起来很有趣,因为它是第一个不合适的地方。

SELECT p.*
    , u.name user_name, u.status user_status
    , c.name city_name, t.name town_name, d.name dist_name
    , pm.meta_name, pm.meta_email, pm.meta_phone
    , (SELECT concat("{", 
        '"id":"', pc.id, '",', 
        '"content":"', replace(pc.content, '"', '\\"'), '",', 
        '"date":"', pc.date, '",', 
        '"user_id":"', pcu.id, '",', 
        '"user_name":"', pcu.name, '"}"') last_comment_json 
        FROM post_comments pc 
        LEFT JOIN users pcu ON (pcu.id = pc.user_id) 
        WHERE pc.post_id = p.id
        ORDER BY pc.id DESC LIMIT 1) AS last_comment
FROM (
    SELECT id
    FROM posts p
    WHERE p.status = 'published'
    ORDER BY 
        (CASE WHEN p.created_at >= unix_timestamp(now() - INTERVAL p.reputation DAY) 
            THEN +p.reputation ELSE NULL END) DESC, 
        p.id DESC
    LIMIT 0,10
) ids
JOIN posts p ON ids.id = p.id  -- mind the join for the p data
LEFT JOIN users u ON (u.id = p.user_id)
LEFT JOIN citys c ON (c.id = p.city_id)
LEFT JOIN towns t ON (t.id = p.town_id)
LEFT JOIN dists d ON (d.id = p.dist_id)
LEFT JOIN post_metas pm ON (pm.post_id = p.id)
;

这是第一步,但即使现在您也可以看到,您不需要为不需要的行进行这些无用的 LEFT JOINS 和 json 序列化。 (我跳过了GROUP BY p.id,因为我看不到哪个 LEFT JOIN 可能会导致多行,所以你不要进行任何聚合)。

还没有写:

  • 索引
  • 重新制定 CASE 子句(使用 UNION ALL)
  • 可能会强制索引

【讨论】:

  • 首先,感谢您抽出宝贵时间。真的很神奇,谢谢! :) 正如你所说,我想我对 MySQL 的工作原理了解了很多。之前:没有任何 p.reputation 东西排序,但只有 p.id, 5 qry; (0.286+0.286+0.283+0.287+0.247) / 5 = 0.277sec 之后:使用您使用 p.reputation 和 p.id 排序的解决方案,5 qry; (0.399+0.328+0.348+0.329+0.340) / 5 = 0.348sec.BTW,连接影响查询大约 0.130 秒。我想把所有东西都放在帖子表上,但是在使用关系数据库时听起来不专业。有什么建议吗?
【解决方案2】:

这是你的问题:

  • “ORDER BY 表达式”:必须为表中的每一行计算表达式,然后对整个表进行排序,然后结果通过 LIMIT。
  • 不使用索引:当“col”是索引的一部分时,“ORDER BY col”可以通过按顺序遍历索引来消除排序。这在使用 LIMIT 时非常有效。但是,它在这里不起作用。

有一些方法可以摆脱这种混乱,但您需要说明您拥有多少不同级别的“声誉”(例如 3 或类似“很多”)以及它们在统计上是如何分布的(例如,1 个用户声誉 100 和其余的都为零,或均匀分布)。

编辑

嗯,没有关于“声誉”的统计分布或其可能的取值范围的信息。在这种情况下,让我们使用直截了当的方法:

让我们添加一列“repdate”,其中包含:

repdate = p.created_at + INTERVAL p.reputation DAY

这对应于他们拥有的每个声望点将帖子转移到未来的某一天。然后他们将进行相应的排序。如果 p.created_at 不是 DATETIME,请调整以适应口味。

现在,我们可以简单地“ORDER BY redate DESC”并在上面加上索引,它会很快。

【讨论】:

  • 嗨@peufeu,感谢您的回复。我已经更新了我的问题。
  • 实际上,似乎是相同的解决方案,我也在“选择”阶段尝试了该部分,而不仅仅是“按顺序”。正如你所说,问题是每一行的计算。但有趣的是,即使给定的字段已经被索引,当我将第二个字段添加到 order by 子句时,应用程序会发疯,无法理解。
  • 发疯的确切 sql 查询是什么?
  • 在 order by 之前一切正常(带有一个字段子句),但是如果我添加任何第二个字段来 order by (没关系,有问题的“case”东西或任何东西,否则已经索引)然后它会明显变慢。
  • 你能发布EXPLAIN的查询和结果吗?
【解决方案3】:

也许带有列的索引:idreputationcreated_at 可以帮助加快一点速度,如果您还没有尝试,那将是最简单的解决方案。 DBMS 不必读取这么多数据来计算偏移量、限制受影响的记录。

【讨论】:

  • 嗨@aschoerk,感谢您的回复。我已经更新了我的问题。
  • 快速解决方案可能会有所帮助。您还没有级联索引。 MySQL 应该能够使用新索引的数据来找出以 Taster 方式显示哪些记录,因为与扫描整个表时相比,考虑的数据更少。
  • 我按照您的建议尝试了创建一个包含 id、reputation 和 created_at 字段的新索引,但很遗憾没有奏效。
  • 它甚至尝试过使用新的索引吗?也许首先尝试使用较小的选择列表,以便只返回索引中的列。稍后您可以使用该 ID 来获取其余部分。如果较小的版本有帮助,可以讨论实现方式。
  • 好的,。我希望值得一试。如果 DBMS 使用它并且没有更快,那是错误的方法。也许单条记录本身已经不是很长了。
【解决方案4】:
select * 
from (
  SELECT -- everything is ok here
  , CASE 
      WHEN p.created_at >= unix_timestamp(now() - INTERVAL p.reputation DAY) 
        THEN + p.reputation ELSE NULL END order_col
  FROM posts AS p
  WHERE p.status = 'published' -- the only thing for filter
  LIMIT 0,10 -- limit provided as well
) a
ORDER BY 
    a.order_col desc
    ,a.id DESC

【讨论】:

  • 虽然这样更快,但只有在极少数情况下才会产生相同的结果。它的作用是:它需要 10 行(随机)行(内部查询),然后对 10 行进行排序。
  • 已经试过了,可惜没用,因为运行在相同的计算规则上。
【解决方案5】:
  • Inflate-deflate -- LEFT JOIN 增加行数,GROUP BY 然后放气。膨胀的行数是昂贵的。相反,请专注于在执行任何JOINing之前获取所需行的 ID。运气好的话,你可以摆脱GROUP BY

  • WP 架构 -- 这是一个 EAV 架构,在性能和扩展方面很糟糕。

  • 您有哪些索引?请参阅this 了解如何改进元表。

  • 复杂的ORDER BY。这导致在排序和执行LIMIT 之前收集所有行(过滤后)。如果可能,重新考虑ORDER BY 子句。

在你完成了我的建议后,开始另一个问题以继续完善。请务必包含EXPLAIN SELECT ...SHOW CREATE TABLE

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-31
    • 2011-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多