MySQL 使用 Order By 时查询变得非常慢
MySQL query becomes extremely slow when using Order By
我有一个 messages
table 有 1500 万行。
以下查询returns约500万条(但限制在15行)不到1秒的记录:
SELECT messages.* FROM messages
INNER JOIN gateways ON
messages.gateway_id=gateways.id
INNER JOIN orders ON
gateways.order_id=orders.id
WHERE orders.user_id=6500 AND messages.deleted=0
AND messages.type='Out' LIMIT 15;
但是当我在它的末尾添加一个 Order By
idDESC
时,它变得非常慢大约 40 秒:
SELECT messages.* FROM messages
INNER JOIN gateways ON
messages.gateway_id=gateways.id
INNER JOIN orders ON
gateways.order_id=orders.id
WHERE orders.user_id=6500 AND messages.deleted=0
AND messages.type='Out' ORDER BY messages.id DESC LIMIT 15;
如有任何帮助,我们将不胜感激。
模式 SELECT lots_of_stuff ORDER BY something LIMIT small_integer
因导致性能问题而臭名昭著。遗漏 ORDER BY something
会使性能问题消失。为什么?因为带有 ORDER BY
的模式导致 MySQL 服务器对大量相当大的行(在您的情况下为 500 万)进行排序,只丢弃其中的一小部分。这会在您的服务器中使用大量 RAM、CPU 和 IO,只是为了放弃大部分工作。
您最好的选择是在此处使用延迟连接类型的模式,您只对 message.id
值进行排序。使用此子查询来执行此操作。
SELECT messages.id
FROM messages
INNER JOIN gateways ON messages.gateway_id=gateways.id
INNER JOIN orders ON gateways.order_id=orders.id
WHERE orders.user_id=6500
AND messages.deleted=0
AND messages.type='Out'
ORDER BY messages.id DESC
LIMIT 15
这将为您提供 15 个 message.id
值的精美小集合。
您的下一步是优化此子查询。我建议您在 messages
table 上尝试使用包含列 (deleted, type, id, gateway_id)
的复合覆盖索引。这应该有助于加速它。
您可能还需要其他 table 的索引。您应该考虑使用 MySQL 中的 EXPLAIN
函数来分析您的表现。
最后,使用 messages.id
值的小集合来获取您需要的 messages
行,就像这样。 (这是延迟连接;您将延迟获取整行,直到您知道需要哪些行。这样您就不必 ORDER
搞得一团糟。)
编辑 在 gateways (order_id, id)
上添加复合索引以避免 table 的完整 table 扫描。它并不大,但这可能会有所帮助。
SELECT a.*
FROM messages a
JOIN (
SELECT messages.id
FROM messages
INNER JOIN gateways ON messages.gateway_id=gateways.id
INNER JOIN orders ON gateways.order_id=orders.id
WHERE orders.user_id=6500
AND messages.deleted=0
AND messages.type='Out'
ORDER BY messages.id DESC
LIMIT 15
) b ON a.id = b.id
ORDER BY a.id DESC
我假设
- 每个订单属于一个用户
- 每个网关属于一个订单
因此,这:
INNER JOIN gateways ON messages.gateway_id=gateways.id
INNER JOIN orders ON gateways.order_id=orders.id
WHERE orders.user_id=6500 AND messages.deleted=0
可以改写成英文:
"Get the gateways which belong to the orders which belong to this user".
现在,要获取与此用户相关的最新消息,问题是我们可能会有许多不同的 gateway_id(根据您的解释,大约有 143 条),因此我们不能使用跳过排序的索引。
好吧,正如 O. Jones 所展示的那样,我们可以做到,但有一个问题。这是查询的简化版本:
SELECT ... FROM messages
WHERE gateway_id IN (1,2) ORDER BY id DESC LIMIT 10
如果我们在 (id,gateway_id) 上有一个索引,那么 MySQL 很可能决定按降序扫描它。如果它快速找到 10 条具有 "gateway_id IN (1,2)" 的消息,那么它会很快。但是,如果这些 gateway_id 有非常旧的消息,或者根本没有消息,它可能需要扫描整个索引。
如果 PK 关系如我所述,我将在消息 table 中具体化一个 user_id 列,然后允许在 (user_id,message_id) 这将使查询时间远低于一毫秒。
我有一个 messages
table 有 1500 万行。
以下查询returns约500万条(但限制在15行)不到1秒的记录:
SELECT messages.* FROM messages
INNER JOIN gateways ON
messages.gateway_id=gateways.id
INNER JOIN orders ON
gateways.order_id=orders.id
WHERE orders.user_id=6500 AND messages.deleted=0
AND messages.type='Out' LIMIT 15;
但是当我在它的末尾添加一个 Order By
idDESC
时,它变得非常慢大约 40 秒:
SELECT messages.* FROM messages
INNER JOIN gateways ON
messages.gateway_id=gateways.id
INNER JOIN orders ON
gateways.order_id=orders.id
WHERE orders.user_id=6500 AND messages.deleted=0
AND messages.type='Out' ORDER BY messages.id DESC LIMIT 15;
如有任何帮助,我们将不胜感激。
模式 SELECT lots_of_stuff ORDER BY something LIMIT small_integer
因导致性能问题而臭名昭著。遗漏 ORDER BY something
会使性能问题消失。为什么?因为带有 ORDER BY
的模式导致 MySQL 服务器对大量相当大的行(在您的情况下为 500 万)进行排序,只丢弃其中的一小部分。这会在您的服务器中使用大量 RAM、CPU 和 IO,只是为了放弃大部分工作。
您最好的选择是在此处使用延迟连接类型的模式,您只对 message.id
值进行排序。使用此子查询来执行此操作。
SELECT messages.id
FROM messages
INNER JOIN gateways ON messages.gateway_id=gateways.id
INNER JOIN orders ON gateways.order_id=orders.id
WHERE orders.user_id=6500
AND messages.deleted=0
AND messages.type='Out'
ORDER BY messages.id DESC
LIMIT 15
这将为您提供 15 个 message.id
值的精美小集合。
您的下一步是优化此子查询。我建议您在 messages
table 上尝试使用包含列 (deleted, type, id, gateway_id)
的复合覆盖索引。这应该有助于加速它。
您可能还需要其他 table 的索引。您应该考虑使用 MySQL 中的 EXPLAIN
函数来分析您的表现。
最后,使用 messages.id
值的小集合来获取您需要的 messages
行,就像这样。 (这是延迟连接;您将延迟获取整行,直到您知道需要哪些行。这样您就不必 ORDER
搞得一团糟。)
编辑 在 gateways (order_id, id)
上添加复合索引以避免 table 的完整 table 扫描。它并不大,但这可能会有所帮助。
SELECT a.*
FROM messages a
JOIN (
SELECT messages.id
FROM messages
INNER JOIN gateways ON messages.gateway_id=gateways.id
INNER JOIN orders ON gateways.order_id=orders.id
WHERE orders.user_id=6500
AND messages.deleted=0
AND messages.type='Out'
ORDER BY messages.id DESC
LIMIT 15
) b ON a.id = b.id
ORDER BY a.id DESC
我假设
- 每个订单属于一个用户
- 每个网关属于一个订单
因此,这:
INNER JOIN gateways ON messages.gateway_id=gateways.id
INNER JOIN orders ON gateways.order_id=orders.id
WHERE orders.user_id=6500 AND messages.deleted=0
可以改写成英文:
"Get the gateways which belong to the orders which belong to this user".
现在,要获取与此用户相关的最新消息,问题是我们可能会有许多不同的 gateway_id(根据您的解释,大约有 143 条),因此我们不能使用跳过排序的索引。
好吧,正如 O. Jones 所展示的那样,我们可以做到,但有一个问题。这是查询的简化版本:
SELECT ... FROM messages
WHERE gateway_id IN (1,2) ORDER BY id DESC LIMIT 10
如果我们在 (id,gateway_id) 上有一个索引,那么 MySQL 很可能决定按降序扫描它。如果它快速找到 10 条具有 "gateway_id IN (1,2)" 的消息,那么它会很快。但是,如果这些 gateway_id 有非常旧的消息,或者根本没有消息,它可能需要扫描整个索引。
如果 PK 关系如我所述,我将在消息 table 中具体化一个 user_id 列,然后允许在 (user_id,message_id) 这将使查询时间远低于一毫秒。