优化ORDER BY

时间:2017-07-30 15:12:35

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

我正在尝试优化此查询,按posts字段(第1个)和reputation字段(第2个)对id进行排序。没有第一个字段查询需要~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)
- id表格上的小学posts - 字段的索引为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', 'Using filesort'

UPDATE ^^

声望提供:一个帖子,列表顶部可以显示多少(n =声誉)日。

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

UPDATE ^^

-- 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, Using index condition; Using temporary; Using filesort
# dunno, these join's are not using, but if i remove returning fields from select part show "Using index condition"
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, Using index condition; Using where; Using filesort
2, DEPENDENT SUBQUERY, pcu, , eq_ref, PRIMARY, PRIMARY, 2, pc.user_id, 1, 100.00, 

5 个答案:

答案 0 :(得分:1)

这是你的问题:

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

有很多方法可以解决这个问题,但你需要告诉我们有多少不同级别的声誉"你有(像3,或喜欢"很多")以及它们如何在统计上分布(例如,1名声望为100的用户,其余的都是零,或均匀分布)。

修改

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

让我们添加一个列" repdate"其中包含:

repdate = p.created_at + INTERVAL p.reputation DAY

这相当于将他们拥有的每个声望点转移到未来一天。然后他们会相应地排序。如果p.created_at不是DATETIME,请调整味道。

现在,我们可以简单地通过重新命名DESC"并且有一个索引,它会很快。

答案 1 :(得分:1)

这是一个非常有趣的查询。在优化过程中,您可能会发现并了解有关MySQL如何工作的许多新信息。我不确定我是否会有时间详细编写所有内容,但我可以逐步更新。

为什么它很慢

基本上有两种情况:快速

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

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

要修复的第一件事

在许多情况下,您无法构建允许您遵循其顺序的索引(例如,当您从不同的表中对ORDER BY列进行排序时),因此在这种情况下的经验法则是最小化数据MySQL将放入临时表。你怎么能这样做?您只选择子查询中行的标识符,并在获得ID之后,将ids连接到表本身和其他表以获取内容。那就是你用订单制作一张小桌子,然后使用快速方案。 (这通常与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)
  • 可能强制索引

答案 2 :(得分:0)

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

答案 3 :(得分:0)

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

答案 4 :(得分:0)

  • Inflate-deflate - LEFT JOIN会增加行数,GROUP BY然后缩小。膨胀的行数很昂贵。而是专注于在执行任何JOINing之前获取所需行的ID。幸运的话,你可以摆脱GROUP BY

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

  • 你有什么指数?有关如何改进元表的信息,请参阅this

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

在完成我的建议后,启动另一个问题以继续完善。请务必加入EXPLAIN SELECT ...SHOW CREATE TABLE