SQL:在超过 1500 万行的查询中结合 WHERE、ORDER 和 LIMIT
SQL: Combining WHERE, ORDER and LIMIT on 15 million+ row query
我有 2 个 table,item
和 config
。
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 个,您有(除其他外)以下选项:
使用按 item_name
排序的索引并继续阅读,直到找到 200 行也满足 config_id=678
(无需排序)
使用 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 行)。
那么,你能做什么:
- 您可以 force MySQL 使用您想要的索引,例如
... from item t0 force index (ITEM_FK_1) left outer join ...
。这样做的缺点是,根据 id,不同的执行计划可能会更快。
- 你可以添加一个最优索引:复合索引
(config_id, item_name)
允许您只读取具有正确 ID 的行,并且由于它们按名称排序,您可以在前 200 行后停止。无论您的数据分布如何,您总是读取 200 行(或更少)。假设 id
是主键,没有比这更快的解决方案了。
我会选择选项 2。
添加这个
INDEX(config_id, item_name, id) -- in this order!
并且 DROP
任何索引是该索引的 'prefix'。
我有 2 个 table,item
和 config
。
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 个,您有(除其他外)以下选项:
使用按
item_name
排序的索引并继续阅读,直到找到 200 行也满足config_id=678
(无需排序)使用
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 行)。
那么,你能做什么:
- 您可以 force MySQL 使用您想要的索引,例如
... from item t0 force index (ITEM_FK_1) left outer join ...
。这样做的缺点是,根据 id,不同的执行计划可能会更快。 - 你可以添加一个最优索引:复合索引
(config_id, item_name)
允许您只读取具有正确 ID 的行,并且由于它们按名称排序,您可以在前 200 行后停止。无论您的数据分布如何,您总是读取 200 行(或更少)。假设id
是主键,没有比这更快的解决方案了。
我会选择选项 2。
添加这个
INDEX(config_id, item_name, id) -- in this order!
并且 DROP
任何索引是该索引的 'prefix'。