优化 ORDER BY
Optimizing ORDER BY
我正在尝试优化此查询,该查询按 reputation
字段(第 1 个)然后 id
字段(第 2 个)对 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)
- 主要是 id
posts
table
- 字段索引为 created_at
和 reputation
解释结果:
# 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'
更新^^
Reputation 规定:post,有多少(n=reputation)天可以显示在列表顶部。
实际上,我试图为一些可以在列表顶部获取的 post 提供声望,并找到解决方案:。但是一段时间后(大约 2 年),由于 table 数据量增加,该解决方案现在成为一个问题。如果我无法解决这个问题,那么我应该从服务中删除该功能。
更新^^
-- 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,
这是你的问题:
- "ORDER BY expression":table中的每一行都要计算表达式,然后对整个table进行排序,然后结果经过LIMIT。
- 不使用索引:"ORDER BY col" 当 "col" 是索引的一部分时,可以通过按顺序遍历索引来消除排序。这在使用 LIMIT 时非常有效。但是,它在这里不起作用。
有很多方法可以解决这个问题,但您需要说明您有多少个不同级别的 "reputation"(例如 3,或 "a lot")以及它们的统计分布情况(例如,1 个用户的信誉为 100,其余用户的信誉均为零或均匀分布)。
编辑
嗯,没有关于 "reputation" 的统计分布或其可能值范围的信息。在这种情况下,让我们采用直截了当的方法:
让我们添加一列 "repdate",其中包含:
repdate = p.created_at + INTERVAL p.reputation DAY
这对应于他们拥有的每个声望点将帖子转移到未来一天。然后他们将相应地排序。如果 p.created_at 不是 DATETIME,请根据口味进行调整。
现在,我们可以简单地 "ORDER BY repdate DESC" 加上索引,速度会很快。
也许包含列的索引:id
、reputation
、created_at
可以帮助加快一点速度,如果您还没有尝试过,那将是最简单的解决方案. DBMS 就不必读取那么多数据,来计算偏移量、限制-受影响的记录。
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
这是一个非常有趣的查询。在其优化过程中,您可能会发现并了解有关 MySQL 工作原理的许多新信息。我不确定我是否有时间一次详细写所有内容,但我可以逐渐更新。
为什么速度慢
基本上有两种情况:快和慢。
在 quick 场景中,您正以某种预定义的顺序遍历 table 并且可能同时通过 id 从其他行快速获取一些数据table秒。在这种情况下,一旦您的 LIMIT 子句指定了足够的行,您就停止行走。订单从哪里来?来自 table 上的 b 树索引或子查询中结果集的顺序。
在慢场景中,您没有预定义的顺序,MySQL必须隐式地将所有数据放入临时table,对table 在某些字段上,return 来自 LIMIT 子句的 n 行。如果您放入该临时 table 的任何字段的类型为 TEXT(不是 VARCHAR),MySQL 甚至不会尝试将 table 保留在 RAM 中并将其刷新并排序磁盘(因此需要额外的 IO 处理)。
首先要解决的问题
在很多情况下,您无法建立一个允许您遵循其顺序的索引(例如,当您对来自不同 table 的列进行 ORDER BY 时),因此经验法则情况是尽量减少 MySQL 将放入临时 table 的数据。你怎么能这样做?您 select 只是子查询中行的标识符,在获得 ID 之后,您将 ID 连接到 table 本身和其他 table 以获取内容。也就是你做一个小table一个订单,然后使用快速场景。 (这与一般的 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)
- 可能会强制索引
膨胀-收缩——LEFT JOIN
膨胀行数,GROUP BY
然后收缩。膨胀的行数代价高昂。相反,在 执行任何 JOINing
之前,请专注于获取所需行的 ID 。运气好的话,你可以去掉 GROUP BY
.
WP 模式 -- 这是一个 EAV 模式,在性能和缩放方面很糟糕。
你有什么索引?请参阅 this 了解如何改进 meta table。
复杂ORDER BY
。这导致在排序和执行 LIMIT
之前收集所有行(过滤后)。如果可能,请重新考虑 ORDER BY
子句。
在你按照我的建议做了你力所能及的事情之后,开始另一个问题继续完善。请务必包括 EXPLAIN SELECT ...
和 SHOW CREATE TABLE
.
我正在尝试优化此查询,该查询按 reputation
字段(第 1 个)然后 id
字段(第 2 个)对 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)
- 主要是 id
posts
table
- 字段索引为 created_at
和 reputation
解释结果:
# 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'
更新^^
Reputation 规定:post,有多少(n=reputation)天可以显示在列表顶部。
实际上,我试图为一些可以在列表顶部获取的 post 提供声望,并找到解决方案:
更新^^
-- 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,
这是你的问题:
- "ORDER BY expression":table中的每一行都要计算表达式,然后对整个table进行排序,然后结果经过LIMIT。
- 不使用索引:"ORDER BY col" 当 "col" 是索引的一部分时,可以通过按顺序遍历索引来消除排序。这在使用 LIMIT 时非常有效。但是,它在这里不起作用。
有很多方法可以解决这个问题,但您需要说明您有多少个不同级别的 "reputation"(例如 3,或 "a lot")以及它们的统计分布情况(例如,1 个用户的信誉为 100,其余用户的信誉均为零或均匀分布)。
编辑
嗯,没有关于 "reputation" 的统计分布或其可能值范围的信息。在这种情况下,让我们采用直截了当的方法:
让我们添加一列 "repdate",其中包含:
repdate = p.created_at + INTERVAL p.reputation DAY
这对应于他们拥有的每个声望点将帖子转移到未来一天。然后他们将相应地排序。如果 p.created_at 不是 DATETIME,请根据口味进行调整。
现在,我们可以简单地 "ORDER BY repdate DESC" 加上索引,速度会很快。
也许包含列的索引:id
、reputation
、created_at
可以帮助加快一点速度,如果您还没有尝试过,那将是最简单的解决方案. DBMS 就不必读取那么多数据,来计算偏移量、限制-受影响的记录。
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
这是一个非常有趣的查询。在其优化过程中,您可能会发现并了解有关 MySQL 工作原理的许多新信息。我不确定我是否有时间一次详细写所有内容,但我可以逐渐更新。
为什么速度慢
基本上有两种情况:快和慢。
在 quick 场景中,您正以某种预定义的顺序遍历 table 并且可能同时通过 id 从其他行快速获取一些数据table秒。在这种情况下,一旦您的 LIMIT 子句指定了足够的行,您就停止行走。订单从哪里来?来自 table 上的 b 树索引或子查询中结果集的顺序。
在慢场景中,您没有预定义的顺序,MySQL必须隐式地将所有数据放入临时table,对table 在某些字段上,return 来自 LIMIT 子句的 n 行。如果您放入该临时 table 的任何字段的类型为 TEXT(不是 VARCHAR),MySQL 甚至不会尝试将 table 保留在 RAM 中并将其刷新并排序磁盘(因此需要额外的 IO 处理)。
首先要解决的问题
在很多情况下,您无法建立一个允许您遵循其顺序的索引(例如,当您对来自不同 table 的列进行 ORDER BY 时),因此经验法则情况是尽量减少 MySQL 将放入临时 table 的数据。你怎么能这样做?您 select 只是子查询中行的标识符,在获得 ID 之后,您将 ID 连接到 table 本身和其他 table 以获取内容。也就是你做一个小table一个订单,然后使用快速场景。 (这与一般的 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)
- 可能会强制索引
膨胀-收缩——
LEFT JOIN
膨胀行数,GROUP BY
然后收缩。膨胀的行数代价高昂。相反,在 执行任何JOINing
之前,请专注于获取所需行的 ID 。运气好的话,你可以去掉GROUP BY
.WP 模式 -- 这是一个 EAV 模式,在性能和缩放方面很糟糕。
你有什么索引?请参阅 this 了解如何改进 meta table。
复杂
ORDER BY
。这导致在排序和执行LIMIT
之前收集所有行(过滤后)。如果可能,请重新考虑ORDER BY
子句。
在你按照我的建议做了你力所能及的事情之后,开始另一个问题继续完善。请务必包括 EXPLAIN SELECT ...
和 SHOW CREATE TABLE
.