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 ByidDESC 时,它变得非常慢大约 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) 这将使查询时间远低于一毫秒。