SQL:在超过 1500 万行的查询中结合 WHERE、ORDER 和 LIMIT

SQL: Combining WHERE, ORDER and LIMIT on 15 million+ row query

我有 2 个 table,itemconfig

item 约有 1500 万行,config 约有 1000 行。

我想用 WHERE 子句连接两个 table 并对结果进行排序。

这可能看起来像这样:

SELECT
    `t0`.`id`,
    `t0`.`item_name`,
    `t1`.`id`,
    `t1`.`config_name`,
FROM
    `item` t0
    LEFT OUTER JOIN `config` `t1` ON `t0`.`config_id` = `t1`.`id`
WHERE (`t0`.`config_id` = 678)
ORDER BY
    `t0`.`item_name` ASC;

这 运行 在 ~800 毫秒和 returns ~50k 行内成功。

我也想对这个结果进行分页,所以我 运行 相同的查询并添加 LIMIT:

SELECT
    `t0`.`id`,
    `t0`.`item_name`,
    `t1`.`id`,
    `t1`.`config_name`,
FROM
    `item` t0
    LEFT OUTER JOIN `config` `t1` ON `t0`.`config_id` = `t1`.`id`
WHERE (`t0`.`config_id` = 678)
ORDER BY
    `t0`.`item_name` ASC LIMIT 200;

此查询现在需要超过 5 分钟

我想了解造成这种差异的原因。

我可以简化查询,完全删除 JOIN,仅查询较大的 table 以尝试找出速度变慢的原因:

SELECT
    `t0`.`id`,
    `t0`.`item_name`,
FROM
    `item` t0
WHERE (`t0`.`config_id` = 678)
ORDER BY
    `t0`.`item_name` ASC;

这个查询 运行 没问题,但是再次添加 LIMIT 会大大增加查询时间。

我该如何解决这个问题或更好地诊断是什么原因造成的?

没有LIMIT的简化查询的执行计划如下:

+----+-------------+-------+------------+------+---------------+-----------+---------+-------+-------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys |    key    | key_len |  ref  | rows  | filtered |                 extra                 |
+----+-------------+-------+------------+------+---------------+-----------+---------+-------+-------+----------+---------------------------------------+
|  1 | SIMPLE      | t0    | NULL       | ref  | ITEM_FK_1     | ITEM_FK_1 |       8 | const | 98524 |   100.00 | Using index condition; Using filesort |
+----+-------------+-------+------------+------+---------------+-----------+---------+-------+-------+----------+---------------------------------------+

LIMIT 200 添加到查询会生成此执行计划:

+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+-------+----------+--------------------------+
| id | select_type | table | partitions | type  | possible_keys |        key         | key_len | ref  | rows  | filtered |          extra           |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+-------+----------+--------------------------+
|  1 | SIMPLE      | t0    | NULL       | index | ITEM_FK_1     | ITEM_RULE_ITEM_UNQ |     775 | NULL | 31933 |     0.63 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+-------+----------+--------------------------+

要查找具有 config_id=678 的行并按 item_name 排序并仅取前 200 个,您有(除其他外)以下选项:

  1. 使用按 item_name 排序的索引并继续阅读,直到找到 200 行也满足 config_id=678(无需排序)

  2. 使用 config_id(您的外键)上的索引获取所有具有 config_id=678 的行,然后按名称对这些行进行排序,并取前 200

哪个更快取决于您的数据。

首先,这取决于 config_id=678 行的位置。如果例如前 200 行(按名称排序,例如以 A 开头)都有此 ID,这将非常快:您可以读取 200 行,然后停止,甚至不必订购任何东西。如果你运气不好,所有这些 id 都在这个列表的末尾(例如,只有以 Z 开头的名字才有这个 id),你必须阅读所有行才能找到 200 个合适的。

第二个选项取决于 config_id=678 的行数。它将读取所有 50k(使用索引),对它们进行排序,并为您提供前 200 个。这将介于上面的快速和慢速选项之间。

MySQL 现在基本上必须猜测哪个版本更快。对于 limit 200 的查询,它猜错了,显然,它必须读取比预期更多的行。

让您了解 MySQL 的想法:

  • MySQL 假设您有 98.524 行(不是 50k)和 config_id=678(第一个执行计划中 rows 中的数字)。

  • 您有 1500 万行,因此特定行具有该 ID 的概率为 98.524 / 15 Mill = 1/150。您需要其中的 200 个,因此您需要阅读大约 200*150=30.000(或 31.933,第二个执行计划中的数字)行,直到 可能 找到足够的行。

现在 MySQL 将读取 100k 行并对其排序与 可能 读取 30k 行进行比较,并选择了后者。在这种情况下是错误的(虽然 5 分钟似乎有点多,但还有其他因素,如索引大小增加或可能缺少覆盖率可能会减慢速度)。但可能适用于不同的 ID。

如果你增加限制(你将不得不为后面的页面做),MySQL 将在某个时候切换执行计划(例如找到第一个 1.000 的概率需要大约 1.000*150= 150k > 100k 行)。

那么,你能做什么:

  1. 您可以 force MySQL 使用您想要的索引,例如... from item t0 force index (ITEM_FK_1) left outer join ...。这样做的缺点是,根据 id,不同的执行计划可能会更快。
  2. 你可以添加一个最优索引:复合索引(config_id, item_name) 允许您只读取具有正确 ID 的行,并且由于它们按名称排序,您可以在前 200 行后停止。无论您的数据分布如何,您总是读取 200 行(或更少)。假设 id 是主键,没有比这更快的解决方案了。

我会选择选项 2。

添加这个

INDEX(config_id, item_name,  id)   -- in this order!

并且 DROP 任何索引是该索引的 'prefix'。