MySQL同样条件的SQL,多查询1个字段得到的记录却不一样?MySQL内部优化机制引起未预期的查询结果

问题描述

带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的两条记录

解决方案

确定了原因之后,解决方案就很容易想到了,根据不同情况可以想到以下几种解决方案

  1. 如果可以,不要写这种用重复值的索引排序,排序的索引列最好是唯一索引
  2. 如果必须要用这种查询,可以考虑force index,确保两种SQL走一样的查询方式:select id, name, value from test_table force index(idx_name) order by name limit 400000, 3,当然force index有可能会导致查询变慢(也有可能会更快),要考虑清楚再使用
  3. 期待评论区补充

仔细想来,这会不会算是MySQL内部优化机制导致的一个bug呢?如果没有默认优化,查询方式一样,那么结果也会是一样的,优化之后可能会导致上面这种查询结果不一样的情况,而且官方也宣称了优化也不一定保证是更快的,有时可以考虑自己加上force index不走扫表方式查询

你可能感兴趣的:(数据库,mysql)