K-G*_*Gun 6 mysql sql-order-by query-optimization
我正在尝试优化此查询,该查询posts按reputation字段(第一个)排序,然后按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
Run Code Online (Sandbox Code Playgroud)
注意:
- 使用 InnoDB (MySQL 5.7.19)
- 主数据库位于表id上
- 字段均被索引和postscreated_atreputation
解释结果:
# id、select_type、表、分区、类型、possible_keys、key、key_len、ref、行、过滤、额外 # '1', 'SIMPLE', 'p', NULL, 'ALL', NULL, NULL, NULL, NULL, '31968', '100.00', '使用文件排序'
更新^^
声誉规定:一个帖子,多少(n=声誉)天可以显示在列表顶部。
实际上,我试图为一些可以在列表顶部获取的帖子提供声誉,并找到解决方案:按“代表”排序帖子,但仅限“一天”限制。但一段时间后(大约两年),由于表数据量的增加,该解决方案现在成为一个问题。如果我无法解决此问题,那么我应该从服务中删除该功能。
更新^^
-- 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;
Run Code Online (Sandbox Code Playgroud)
解释;
# id、select_type、表、分区、类型、possible_keys、key、key_len、ref、行、过滤、额外 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, 主, u, , eq_ref, 主, 主, 2, p.user_id, 1, 100.00, 1, 主, c, , eq_ref, 主, 主, 1, p.city_id, 1, 100.00, 1, 主, t, , eq_ref, 主, 主, 2, p.town_id, 1, 100.00, 1, 主, d, , eq_ref, 主, 主, 2, p.dist_id, 1, 100.00, 1, 主, pp, , eq_ref, 主, 主, 2, p.id, 1, 100.00, 2, 依赖子查询, 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,
这是一个非常有趣的查询。在优化过程中,您可能会发现并了解很多有关 MySQL 工作原理的新信息。我不确定我是否有时间一次详细地写出所有内容,但我可以逐渐更新。
基本上有两种情况:快速和慢速。
在快速场景中,您正在按某种预定义的顺序遍历表,并且可能同时通过 id 从其他表中的每一行快速获取一些数据。在这种情况下,一旦 LIMIT 子句指定了足够的行,您就会停止行走。订单从哪里来?来自表上的 B 树索引或子查询中结果集的顺序。
在缓慢的场景中,您没有预定义的顺序,MySQL 必须隐式地将所有数据放入临时表中,根据某些字段对表进行排序,并从 LIMIT 子句中返回 n行。如果放入该临时表的任何字段的类型为 TEXT(不是 VARCHAR),MySQL 甚至不会尝试将该表保留在 RAM 中,而是在磁盘上刷新和排序它(因此需要额外的 IO 处理)。
在很多情况下,您无法构建允许您遵循其顺序的索引(例如,当您对不同表中的列进行 ORDER BY 时),因此在这种情况下的经验法则是尽量减少 MySQL 将放入的数据在临时表中。你怎么能这样做?您仅选择子查询中行的标识符,在获得 ids 后,将 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)
;
Run Code Online (Sandbox Code Playgroud)
这是第一步,但即使现在您也可以看到,您不需要为不需要的行进行这些无用的 LEFT JOINS 和 json 序列化。(我跳过了GROUP BY p.id,因为我看不到哪个 LEFT JOIN 可能会导致多行,因此您不进行任何聚合)。
还没有写:
| 归档时间: |
|
| 查看次数: |
11106 次 |
| 最近记录: |