目录
- 前言
- 1、无 Order by 的随机问题
- 2、Order by + Limit 的随机问题
- 3、聚合函数 + Limit的不准确问题
- 总结
前言
在使用MySQL的过程中,相比大家都踩过不少坑,下面我将列举日常生活中比较高频的三个”坑“,也欢迎大家踊跃分享自己的心路历程 🙃!
1、无 Order by 的随机问题
简单来说,就是在没有 Order By 的情况下,如果SELECT的字段不同,返回的记录顺序是随机的,不一定一样。
因为返回的记录顺序跟插入顺序、主键顺序、以及字段上是否建立了索引等因素都有关联,可以通过下面的例子进行验证:
假设有这样的一张用户点击表:
create table t_user_click
(
id int() auto_increment primary key,
obj varchar() default '' not null comment '点击对象',
click smallint default not null comment '点击数',
remark varchar() default '' not null comment '备注',
created_at timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
updated_at timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
comment '用户点击表';
插入几条测试数据:
INSERT INTO t_user_click (id, obj, click, remark, created_at, updated_at) VALUES (, 'aaa', 10, '', '2022-10-18 14:44:57', '2022-10-18 14:44:57');
INSERT INTO t_user_click (id, obj, click, remark, created_at, updated_at) VALUES (, 'bbb', 20, '', '2022-10-18 14:44:57', '2022-10-18 14:44:57');
INSERT INTO t_user_click (id, obj, click, remark, created_at, updated_at) VALUES (, 'ccc', 30, '', '2022-10-18 14:44:57', '2022-10-18 14:44:57');
INSERT INTO t_user_click (id, obj, click, remark, created_at, updated_at) VALUES (, 'ddd', 40, '', '2022-10-18 14:44:57', '2022-10-18 14:44:57');
INSERT INTO t_user_click (id, obj, click, remark, created_at, updated_at) VALUES (, 'eee', 50, '', '2022-10-18 14:44:57', '2022-10-18 14:44:57');
INSERT INTO t_user_click (id, obj, click, remark, created_at, updated_at) VALUES (, 'fff', 60, '', '2022-10-18 14:44:57', '2022-10-18 14:44:57');
首先来看插入顺序带来的影响,在顺序插入几条顺序后,不管查询多少次,获取的结果都与插入顺序一致:
# 全部字段
mysql> select * from t_user_click where click >;
+----+-----+-------+--------+---------------------+---------------------+
| id | obj | click | remark | created_at | updated_at |
+----+-----+-------+--------+---------------------+---------------------+
| | aaa | 10 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | bbb | 20 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | ccc | 30 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | ddd | 40 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
...
# 与上述获取全部字段的SQL结果一致
mysql> select id, click from t_user_click where click >;
...
其次来看主键顺序这个因素,当该表进行过 DELETE/REPLACE/UPDATE 操作时,不会再按照插入顺序排序了,而是会按照主键ID进行排序。
# 进行update操作
UPDATE t_user_click SET id= WHERE id=3;
# 此时按照主键排序,obj=ccc 变成最后一条,而不是第条
mysql> select * from t_user_click where click >;
+----+-----+-------+--------+---------------------+---------------------+
| id | obj | click | remark | created_at | updated_at |
+----+-----+-------+--------+---------------------+---------------------+
| | aaa | 10 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | bbb | 20 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | ddd | 40 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | eee | 50 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | fff | 60 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | ccc | 30 | | 2022-10-18 14:44:57 | 2022-10-18 14:46:20 |
+----+-----+-------+--------+---------------------+---------------------+
# 与上述获取全部字段的SQL结果一致
mysql> select id, click from t_user_click where click >;
...
最后来看有无索引这个因素,当我们在 click 字段上建立索引时,结果又不一样了:
# 建索引
create index t_user_click_click_index on t_user_click (click);
# id= 在最后一条
mysql> select * from t_user_click where click >;
+----+-----+-------+--------+---------------------+---------------------+
| id | obj | click | remark | created_at | updated_at |
+----+-----+-------+--------+---------------------+---------------------+
| | aaa | 10 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | bbb | 20 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | ddd | 40 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | eee | 50 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | fff | 60 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | ccc | 30 | | 2022-10-18 14:44:57 | 2022-10-18 14:46:20 |
+----+-----+-------+--------+---------------------+---------------------+ rows in set (0.00 sec)
# id= 在第3条
mysql> select id, click from t_user_click where click >;
+----+-------+
| id | click |
+----+-------+
| | 10 |
| | 20 |
| | 30 |
| | 40 |
| | 50 |
| | 60 |
+----+-------+
# 我们可以通过 explain 命令来看两种查询方式的不同:
# 两种方式一个没走索引,一个走了索引,从而导致返回结果的不同
mysql> explain select * from t_user_click where click >;
type:ALL
possible_keys:t_user_click_click_index
key:NULL
Extra:Using where
mysql> explain select id, click from t_user_click where click >;
type:index
possible_keys:t_user_click_click_index
key:t_user_click_click_index
Extra:Using where; Using index
2、Order by + Limit 的随机问题
简单来说就是进行**Order by的字段如果不唯一,则MySQL返回的记录是随机的,常见的表现就是数据分页后出现重复**。
不过,这种随机也不是随机算法那种打乱的随机,它跟数据的插入顺序,以及索引的建立也有一定关系,可以看看下面的例子。
假设有一张这样的用户表:
create table t_user_list
(
id int() auto_increment primary key,
name varchar() default '' not null comment '名称',
age smallint default not null comment '年龄',
created_at timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
updated_at timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
comment '用户表';
当你按顺序插入测试数据,然后再去分页查询,你可能会发现每次返回的结果都是固定的,也不会出现随机的情况(这时候跟插入顺序有关)。
# 插入测试数据
INSERT INTO t_user_list (id, name, age, created_at, updated_at) VALUES (, 'aaa', 1, '2022-10-18 12:55:19', '2022-10-18 12:55:18');
INSERT INTO t_user_list (id, name, age, created_at, updated_at) VALUES (, 'bbb', 2, '2022-10-18 12:55:18', '2022-10-18 12:55:18');
INSERT INTO t_user_list (id, name, age, created_at, updated_at) VALUES (, 'ccc', 3, '2022-10-18 12:55:18', '2022-10-18 12:55:18');
INSERT INTO t_user_list (id, name, age, created_at, updated_at) VALUES (, 'ddd', 4, '2022-10-18 12:55:18', '2022-10-18 12:55:18');
INSERT INTO t_user_list (id, name, age, created_at, updated_at) VALUES (, 'eee', 5, '2022-10-18 12:55:18', '2022-10-18 12:55:18');
INSERT INTO t_user_list (id, name, age, created_at, updated_at) VALUES (, 'ggg', 6, '2022-10-18 12:55:19', '2022-10-18 12:55:19');
INSERT INTO t_user_list (id, name, age, created_at, updated_at) VALUES (, 'iii', 7, '2022-10-18 12:55:17', '2022-10-18 12:55:19');
# 分页查询
mysql> select * from t_user_list order by created_at limit, 3;
+----+------+-----+---------------------+---------------------+
| id | name | age | created_at | updated_at |
+----+------+-----+---------------------+---------------------+
| | iii | 7 | 2022-10-18 12:55:17 | 2022-10-18 12:55:19 |
| | bbb | 2 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
| | ccc | 3 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
+----+------+-----+---------------------+---------------------+ rows in set (0.00 sec)
mysql> select * from t_user_list order by created_at limit, 3;
+----+------+-----+---------------------+---------------------+
| id | name | age | created_at | updated_at |
+----+------+-----+---------------------+---------------------+
| | ddd | 4 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
| | eee | 5 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
| | aaa | 1 | 2022-10-18 12:55:19 | 2022-10-18 12:55:18 |
+----+------+-----+---------------------+---------------------+ rows in set (0.00 sec)
而当你这时候新建一个 created_at 索引,并重新插入新的数据时,如果你再次查询,你会惊奇地发现数据重复了。
# 新建索引
create index t_user_list_created_at_index on t_user_list (created_at);
# 插入新的测试数据
INSERT INTO db_article.t_user_list (id, name, age, created_at, updated_at) VALUES (, 'jjj', 8, '2022-10-18 12:55:18', '2022-10-18 12:55:19');
INSERT INTO db_article.t_user_list (id, name, age, created_at, updated_at) VALUES (, 'kkk', 9, '2022-10-18 12:55:18', '2022-10-18 12:55:19');
INSERT INTO db_article.t_user_list (id, name, age, created_at, updated_at) VALUES (, 'mmm', 10, '2022-10-18 12:55:18', '2022-10-18 12:55:19');
# 再次分页查询
mysql> select * from t_user_list order by created_at limit, 3;
+----+------+-----+---------------------+---------------------+
| id | name | age | created_at | updated_at |
+----+------+-----+---------------------+---------------------+
| | iii | 7 | 2022-10-18 12:55:17 | 2022-10-18 12:55:19 |
| | bbb | 2 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
| | ccc | 3 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
+----+------+-----+---------------------+---------------------+ rows in set (0.00 sec)
# id= 的记录重复了
mysql> select * from t_user_list order by created_at limit, 3;
+----+------+-----+---------------------+---------------------+
| id | name | age | created_at | updated_at |
+----+------+-----+---------------------+---------------------+
| | ccc | 3 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
| | ddd | 4 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
| | eee | 5 | 2022-10-18 12:55:18 | 2022-10-18 12:55:18 |
+----+------+-----+---------------------+---------------------+
关于记录重复的问题,个人的猜想是一开始没有 created_at 索引,此时磁盘的数据块的顺序与插入顺序一致,所以返回的结果一直是固定的;但是加了索引后,并且增加了 created_at 一致的几条数据,导致索引重建数据块顺序发生变化,从而记录重复(瞎猜的,大家可以一起讨论下)
3、聚合函数 + Limit的不准确问题
简单来说,就是在对数据进行 Limit分页时,同时使用聚合函数(比如SUM、COUNT等)对当前分页的结果进行聚合,则最终得到的聚合结果是不准确的。
还是以第一点的例子和数据为例:
mysql> select * from t_user_click;
+----+-----+-------+--------+---------------------+---------------------+
| id | obj | click | remark | created_at | updated_at |
+----+-----+-------+--------+---------------------+---------------------+
| | aaa | 10 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | bbb | 20 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | ddd | 40 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | eee | 50 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | fff | 60 | | 2022-10-18 14:44:57 | 2022-10-18 14:44:57 |
| | ccc | 30 | | 2022-10-18 14:44:57 | 2022-10-18 14:46:20 |
+----+-----+-------+--------+---------------------+---------------------+
# 没分页的总数
mysql> select sum(click) from t_user_click;
+------------+
| sum(click) |
+------------+
| |
+------------+ row in set (0.00 sec)
# 分页后的总数
mysql> select sum(click) from t_user_click limit;
+------------+
| sum(click) |
+------------+
| |
+------------+
# 可以通过子查询解决
select sum(tmp.click) from (select click from t_user_click limit) as tmp;
可以看到,在有Limit的情况下,得到的聚合结果其实是所有记录的总和,并不是三条记录的总和。原因就是SELECT语句执行时有一定顺序,分别是 FROM、ON、JOIN、WHERE、GROUP BY、HAVING、SELECT、DISTINCT、ORDER BY和LIMIT。
在上述SQL中,SELECT优先执行,即先执行 select sum(click) from t_user_click,这时已经得到结果为210,最终再执行 limit 3剔除不符合要求的记录。
总结
最后小结一下,本文主要介绍了MySQL查询中三个比较容易踩的坑,从中我们可以得到:
1、SELECT返回的顺序跟多种因素有关,如插入顺序、主键ID顺序、索引顺序等,如果你返回的结果要求有顺序,则记得加上 order by。
2、加上了 order by 也不要高兴地太早,还需要检查 order by 的字段是否唯一,如果不唯一,返回的结果也有可能是随机的。
3、在进行聚合查询时,切记看有无 LIMIT 子句,有的话记得加上子查询避免查询的结果不准确。