所以说,BNL算法的性能会更好。
# distinct 和 group by的性能
在第37篇文章[《什么时候会使用内部临时表?》](https://time.geekbang.org/column/article/80477)中,@老杨同志 提了一个好问题:如果只需要去重,不需要执行聚合函数,distinct 和group by哪种效率高一些呢?
我来展开一下他的问题:如果表t的字段a上没有索引,那么下面这两条语句:
```
select a from t group by a order by null;
select distinct a from t;
```
的性能是不是相同的?
首先需要说明的是,这种group by的写法,并不是SQL标准的写法。标准的group by语句,是需要在select部分加一个聚合函数,比如:
```
select a,count(*) from t group by a order by null;
```
这条语句的逻辑是:按照字段a分组,计算每组的a出现的次数。在这个结果里,由于做的是聚合计算,相同的a只出现一次。
>
备注:这里你可以顺便复习一下[第37篇文章](https://time.geekbang.org/column/article/80477)中关于group by的相关内容。
没有了count(*)以后,也就是不再需要执行“计算总数”的逻辑时,第一条语句的逻辑就变成是:按照字段a做分组,相同的a的值只返回一行。而这就是distinct的语义,所以不需要执行聚合函数时,distinct 和group by这两条语句的语义和执行流程是相同的,因此执行性能也相同。
这两条语句的执行流程是下面这样的。
创建一个临时表,临时表有一个字段a,并且在这个字段a上创建一个唯一索引;
遍历表t,依次取数据插入临时表中:
1. 如果发现唯一键冲突,就跳过;
1. 否则插入成功;
遍历完成后,将临时表作为结果集返回给客户端。
# 备库自增主键问题
除了性能问题,大家对细节的追问也很到位。在第39篇文章[《自增主键为什么不是连续的?》](https://time.geekbang.org/column/article/80531)评论区,@帽子掉了 同学问到:在binlog_format=statement时,语句A先获取id=1,然后语句B获取id=2;接着语句B提交,写binlog,然后语句A再写binlog。这时候,如果binlog重放,是不是会发生语句B的id为1,而语句A的id为2的不一致情况呢?
首先,这个问题默认了“自增id的生成顺序,和binlog的写入顺序可能是不同的”,这个理解是正确的。
其次,这个问题限定在statement格式下,也是对的。因为row格式的binlog就没有这个问题了,Write row event里面直接写了每一行的所有字段的值。
而至于为什么不会发生不一致的情况,我们来看一下下面的这个例子。
```
create table t(id int auto_increment primary key);
insert into t values(null);
```
可以看到,在insert语句之前,还有一句SET INSERT_ID=1。这条命令的意思是,这个线程里下一次需要用到自增值的时候,不论当前表的自增值是多少,固定用1这个值。
这个SET INSERT_ID语句是固定跟在insert语句之前的,比如@帽子掉了同学提到的场景,主库上语句A的id是1,语句B的id是2,但是写入binlog的顺序先B后A,那么binlog就变成:
```
SET INSERT_ID=2;
语句B;
SET INSERT_ID=1;
语句A;
```
你看,在备库上语句B用到的INSERT_ID依然是2,跟主库相同。
因此,即使两个INSERT语句在主备库的执行顺序不同,自增主键字段的值也不会不一致。
# 小结
今天这篇答疑文章,我选了4个好问题和你分享,并做了分析。在我看来,能够提出好问题,首先表示这些同学理解了我们文章的内容,进而又做了深入思考。有你们在认真的阅读和思考,对我来说是鼓励,也是动力。
说实话,短短的三篇答疑文章无法全部展开同学们在评论区留下的高质量问题,之后有的同学还会二刷,也会有新的同学加入,大家想到新的问题就请给我留言吧,我会继续关注评论区,和你在评论区交流。
老规矩,答疑文章也是要有课后思考题的。
在[第8篇文章](https://time.geekbang.org/column/article/70562)的评论区, @XD同学提到一个问题:他查看了一下innodb_trx,发现这个事务的trx_id是一个很大的数(281479535353408),而且似乎在同一个session中启动的会话得到的trx_id是保持不变的。当执行任何加写锁的语句后,trx_id都会变成一个很小的数字(118378)。
你可以通过实验验证一下,然后分析看看,事务id的分配规则是什么,以及MySQL为什么要这么设计呢?
你可以把你的结论和分析写在留言区,我会在下一篇文章和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
# 上期问题时间
上期的问题是,怎么给分区表t创建自增主键。由于MySQL要求主键包含所有的分区字段,所以肯定是要创建联合主键的。
这时候就有两种可选:一种是(ftime, id),另一种是(id, ftime)。
如果从利用率上来看,应该使用(ftime, id)这种模式。因为用ftime做分区key,说明大多数语句都是包含ftime的,使用这种模式,可以利用前缀索引的规则,减少一个索引。
这时的建表语句是:
```
CREATE TABLE `t` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ftime` datetime NOT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`ftime`,`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = MyISAM,
PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = MyISAM,
PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = MyISAM,
PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = MyISAM);
```
当然,我的建议是你要尽量使用InnoDB引擎。InnoDB表要求至少有一个索引,以自增字段作为第一个字段,所以需要加一个id的单独索引。
```
CREATE TABLE `t` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ftime` datetime NOT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`ftime`,`id`),
KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB,
PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB,
PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB,
PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB);
```
当然把字段反过来,创建成:
```
PRIMARY KEY (`id`,`ftime`),
KEY `id` (`ftime`)
```
也是可以的。
评论区留言点赞板:
>
@夹心面包 、@郭江伟 同学提到了最后一种方案。
>
@aliang 同学提了一个好问题,关于open_files_limit和innodb_open_files的关系,我在回复中做了说明,大家可以看一下。
>
@万勇 提了一个好问题,实际上对于现在官方的版本,将字段加在中间还是最后,在性能上是没差别的。但是,我建议大家养成习惯(如果你是DBA就帮业务开发同学养成习惯),将字段加在最后面,因为这样还是比较方便操作的。这个问题,我也在评论的答复中做了说明,你可以看一下。