MySQL 索引 - 未满 table 在索引中
MySQL Index - not full table is in index
我有一个简单的 InnoDB table,其中包含超过 100 万行和一些简单的索引。
我需要按 first_public
和 id
列对 table 进行排序并获取其中一些,这就是为什么我索引了 first_public
列。
first_public
目前是独一无二的,但在现实生活中可能不是。
mysql> desc table;
+--------------+-------------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| id_category | int | NO | MUL | NULL | |
| active | smallint | NO | | NULL | |
| status | enum('public','hidden') | NO | | NULL | |
| first_public | datetime | YES | MUL | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+--------------+-------------------------+------+-----+---------+----------------+
8 rows in set (0.06 sec)
当我处理 130000+
之前的行时它运行良好
mysql> explain select id from table where active = 1 and status = 'public' order by first_public desc, id desc limit 24 offset 130341;
+----+-------------+--------+------------+-------+---------------+---------------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+---------------+---------------------+---------+------+--------+----------+----------------------------------+
| 1 | SIMPLE | table | NULL | index | NULL | firstPublicDateIndx | 6 | NULL | 130365 | 5.00 | Using where; Backward index scan |
+----+-------------+--------+------------+-------+---------------+---------------------+---------+------+--------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)
但是当我尝试获取下一行(偏移量为 140000+)时,看起来 MySQL 根本不使用 first_public
列索引。
mysql> explain select id from table where active = 1 and status = 'public' order by first_public desc, id desc limit 24 offset 140341;
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
| 1 | SIMPLE | table | NULL | ALL | NULL | NULL | NULL | NULL | 1133533 | 5.00 | Using where; Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
1 row in set, 1 warning (0.00 sec)
我试图在 select
子句中添加 first_public
列,但没有任何改变。
我做错了什么?
MySQL 的优化器会尝试估算执行查询的成本,以决定是否值得使用索引。有时它会比较使用索引与仅按顺序读取行并丢弃不属于结果的行的成本。
在这种情况下,它决定如果您使用大于 140k 的 OFFSET,它会放弃使用索引。
记住 OFFSET 的工作原理。无法通过索引查找偏移量的位置。索引有助于按值而不是位置查找行。所以要执行 OFFSET 查询,它必须检查从第一个匹配行开始的所有行。然后它丢弃检查到偏移量的行,然后计算出足够的行以满足 LIMIT 和 returns 那些。
这就像你想阅读一本书的第 500-510 页,但要做到这一点,你必须先阅读第 1-499 页。然后当有人让你读第511-520页时,你必须再读一遍第1-510页。
最终偏移量变得如此之大,以至于在 table 扫描中读取 14000 行比读取 14000 个索引条目 + 14000 行更便宜。
什么?!? OFFSET真的那么贵吗?是的。按值查找行更为常见,因此 MySQL 针对该用法进行了优化。
因此,如果您可以重新设想分页查询以按值查找行而不是使用 LIMIT/OFFSET,您会更快乐。
例如,假设你读取“page”1000,你看到那个页面的最高id
值是13999。当客户端请求下一页时,你可以做查询:
SELECT ... FROM mytable WHERE id > 13999 LIMIT 24;
这会根据 id
的值进行查找,这是经过优化的,因为它利用了主键索引。然后它只读取 24 行和 returns 它们(MySQL 至少足够聪明,可以在到达 OFFSET + LIMIT 行后停止读取)。
最好的索引是
INDEX(active, status, first_public, id)
使用巨大的偏移量非常低效——它必须扫描超过 140341 + 24 行才能执行查询。
如果您试图“走过”table,请使用“记住您离开的地方”的技巧。更多讨论:http://mysql.rjweb.org/doc.php/pagination
优化器放弃索引的原因:它决定索引和 table 之间的来回跳动 可能 比简单扫描整个 table。 (截止值约为 20%,但差异很大。)
我有一个简单的 InnoDB table,其中包含超过 100 万行和一些简单的索引。
我需要按 first_public
和 id
列对 table 进行排序并获取其中一些,这就是为什么我索引了 first_public
列。
first_public
目前是独一无二的,但在现实生活中可能不是。
mysql> desc table;
+--------------+-------------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| id_category | int | NO | MUL | NULL | |
| active | smallint | NO | | NULL | |
| status | enum('public','hidden') | NO | | NULL | |
| first_public | datetime | YES | MUL | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+--------------+-------------------------+------+-----+---------+----------------+
8 rows in set (0.06 sec)
当我处理 130000+
之前的行时它运行良好mysql> explain select id from table where active = 1 and status = 'public' order by first_public desc, id desc limit 24 offset 130341;
+----+-------------+--------+------------+-------+---------------+---------------------+---------+------+--------+----------+----------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+---------------+---------------------+---------+------+--------+----------+----------------------------------+
| 1 | SIMPLE | table | NULL | index | NULL | firstPublicDateIndx | 6 | NULL | 130365 | 5.00 | Using where; Backward index scan |
+----+-------------+--------+------------+-------+---------------+---------------------+---------+------+--------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)
但是当我尝试获取下一行(偏移量为 140000+)时,看起来 MySQL 根本不使用 first_public
列索引。
mysql> explain select id from table where active = 1 and status = 'public' order by first_public desc, id desc limit 24 offset 140341;
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
| 1 | SIMPLE | table | NULL | ALL | NULL | NULL | NULL | NULL | 1133533 | 5.00 | Using where; Using filesort |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
1 row in set, 1 warning (0.00 sec)
我试图在 select
子句中添加 first_public
列,但没有任何改变。
我做错了什么?
MySQL 的优化器会尝试估算执行查询的成本,以决定是否值得使用索引。有时它会比较使用索引与仅按顺序读取行并丢弃不属于结果的行的成本。
在这种情况下,它决定如果您使用大于 140k 的 OFFSET,它会放弃使用索引。
记住 OFFSET 的工作原理。无法通过索引查找偏移量的位置。索引有助于按值而不是位置查找行。所以要执行 OFFSET 查询,它必须检查从第一个匹配行开始的所有行。然后它丢弃检查到偏移量的行,然后计算出足够的行以满足 LIMIT 和 returns 那些。
这就像你想阅读一本书的第 500-510 页,但要做到这一点,你必须先阅读第 1-499 页。然后当有人让你读第511-520页时,你必须再读一遍第1-510页。
最终偏移量变得如此之大,以至于在 table 扫描中读取 14000 行比读取 14000 个索引条目 + 14000 行更便宜。
什么?!? OFFSET真的那么贵吗?是的。按值查找行更为常见,因此 MySQL 针对该用法进行了优化。
因此,如果您可以重新设想分页查询以按值查找行而不是使用 LIMIT/OFFSET,您会更快乐。
例如,假设你读取“page”1000,你看到那个页面的最高id
值是13999。当客户端请求下一页时,你可以做查询:
SELECT ... FROM mytable WHERE id > 13999 LIMIT 24;
这会根据 id
的值进行查找,这是经过优化的,因为它利用了主键索引。然后它只读取 24 行和 returns 它们(MySQL 至少足够聪明,可以在到达 OFFSET + LIMIT 行后停止读取)。
最好的索引是
INDEX(active, status, first_public, id)
使用巨大的偏移量非常低效——它必须扫描超过 140341 + 24 行才能执行查询。
如果您试图“走过”table,请使用“记住您离开的地方”的技巧。更多讨论:http://mysql.rjweb.org/doc.php/pagination
优化器放弃索引的原因:它决定索引和 table 之间的来回跳动 可能 比简单扫描整个 table。 (截止值约为 20%,但差异很大。)