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

Posted

技术标签:

【中文标题】SQL:在超过 1500 万行查询中结合 WHERE、ORDER 和 LIMIT【英文标题】:SQL: Combining WHERE, ORDER and LIMIT on 15 million+ row query 【发布时间】:2021-12-09 06:48:40 【问题描述】:

我有 2 张桌子,itemconfig

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

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

这可能看起来像这样:

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 毫秒内成功运行并返回约 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,只查询大表来尝试找出变慢的原因:

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 |
+----+-------------+-------+------------+-------+---------------+--------------------+---------+------+-------+----------+--------------------------+

【问题讨论】:

请为这两个查询添加执行计划(explain select ... 的输出)。还要添加现有索引(或创建表语句) - 我假设您在 config_id 和 item_name 上有索引? 感谢@Solarflare - 添加了执行计划 如果你提供SHOW CREATE TABLE会有所帮助。 【参考方案1】:

要查找带有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。

【讨论】:

【参考方案2】:

添加这个

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

以及DROP 任何作为该索引“前缀”的索引。

【讨论】:

谢谢,你能解释一下在此处添加最后一个id 有什么好处吗? @duncanhall - "covering" -- 当所有需要的列都在索引中时,你会得到额外的提升,因为它只需要查看INDEX's BTree。

以上是关于SQL:在超过 1500 万行查询中结合 WHERE、ORDER 和 LIMIT的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Oracle SQL Developer 中执行超过 100 万条插入查询?

如何在 SQL Server 中更新具有数百万行的大表?

优化产品范围查询的性能

PHP PDO 查询,优化超过 1000 万行的速度性能 MS ACCESS 数据库

对超过百万行进行排序的分组

使用 IN 子句和子查询进行极端查询优化