带limit的SQL,相同的条件相同的表,只是多查询了几个字段得到的表的id却不一样,具体来说
两条SQL:
select id, name, value from test_table order by name limit 400000, 3
select id from test_table order by name limit 400000, 3
得到了不同的ID
test_table表,它有id、name和value列,name是一个普通索引
CREATE TABLE `test_table` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`value` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
它一共有60万条数据,如下Python代码,执行两次30万条的数据插入
records = [str((f"xx_{i}", i)) for i in range(300000)]
sql = f"""insert into test_table(name, value) values""" + ','.join(records)
现在分别执行以下两条SQL查询,得到的记录却不一样
select id, name, value from test_table order by name limit 400000, 3
-- 查询结果是
-- 580000 xx_279999 279999
-- 280000 xx_279999 279999
-- 300029 xx_28 28
select id from test_table order by name limit 400000, 3
-- 查询结果是
-- 280000
-- 580000
-- 29
select id, name, value from test_table order by name limit 400000, 3
select id from test_table order by name limit 400000, 3
很明显这两条SQL的条件一模一样,只是查询的列数不相同而已,为什么得到的结果却是不一样的?
当你尝试将两条的条件都稍微改一下时,会发现有时候他们得到的结果又是一样的:
select id, name, value from test_table order by name limit 1000, 3
select id from test_table order by name limit 1000, 3
得到的都是100447、400447、100448对应的记录
既然SQL条件一样,有时却得到结果不一样,那么此时按照逻辑推理应该可以想到是不是MySQL内部做了一些特殊的处理导致结果发生了变化
于是对查询结果不一样的SQL使用explain查看其简要的说明
explain select id, name, value from test_table order by name limit 400000, 3
大概会得到这样的结果
id | select_type | table | type | rows | Extra |
---|---|---|---|---|---|
1 | SIMPLE | test_table | ALL | 598556 | Using filesort |
explain select id from test_table order by name limit 400000, 3
会得到类似这样的结果
id | select_type | table | type | key | key_len | rows | Extra |
---|---|---|---|---|---|---|---|
1 | SIMPLE | test_table | index | idx_name | 131 | 400003 | Using index |
简单来说,第一条多查了几个字段的SQL没有走索引查询而是全表扫描,而第二条SQL走的是索引查询
这里涉及到了深分页查询的MySQL优化机制,
主要原因是MySQL的优化机制认为走全表扫描的查询方式查询效率更高,而通过索引查到id再回表查询的代价更高效率会更低(当然实际情况是也不一定会低,这里就暂且不讨论了)
再对limit 1000的两条语句使用explain查看说明会发现两者查询方式相同,都是走index,因为查询的是浅分页,数据比较靠前,MySQL优化机制认为走index回表查询效率更高,因此没有选择扫表的方式查询
但是你可能会说走不走索引查询的数据不都是一样的吗?为什么得到的结果会不一样?
确实,除了这一层的原因,还有另外一个因素导致了查询结果不一样
回到开头,相同记录的插入执行了两次
# 以下插入操作重复执行两次
records = [str((f"xx_{i}", i)) for i in range(300000)]
sql = f"""insert into test_table(name, value) values""" + ','.join(records)
也就是说name这个字段虽然作为索引,但是它可能有重复的值,想象一下存在重复值的索引数据,以两种不同的排列方式存储,是不是可能会得到不同的数据:
例如
在查询多个字段(全表扫描)时,假设数据按name排序之后是这样的:
id | name | value |
---|---|---|
1 | aaa | 123 |
2 | aaa | 456 |
3 | aaa | 789 |
执行
select id, name, value from table order by name limit 2
获取到的是id为1和2的两条记录
在仅查询索引字段时,假设数据按name排序之后是这样的:
id | name | value |
---|---|---|
1 | aaa | 123 |
3 | aaa | 789 |
2 | aaa | 456 |
(name和value列这里仅方便对比,实际只查询索引时,其他列不会出现)
执行
select id from table order by name limit 2
获取到的就是id为1和id为3的两条记录
确定了原因之后,解决方案就很容易想到了,根据不同情况可以想到以下几种解决方案
select id, name, value from test_table force index(idx_name) order by name limit 400000, 3
,当然force index有可能会导致查询变慢(也有可能会更快),要考虑清楚再使用仔细想来,这会不会算是MySQL内部优化机制导致的一个bug呢?如果没有默认优化,查询方式一样,那么结果也会是一样的,优化之后可能会导致上面这种查询结果不一样的情况,而且官方也宣称了优化也不一定保证是更快的,有时可以考虑自己加上force index不走扫表方式查询