将MySQL去重操作优化到极致之三弹连发(一):巧用索引与变量

        元旦假期收到阿里吴老师来电,被告知已将MySQL查重SQL优化到极致:100万原始数据,其中50万重复,把去重后的50万数据写入目标表只需要9秒钟。这是一个惊人的数字,要知道仅是insert 50万条记录也需要些时间的。于是来了兴趣,自己实验、思考、总结做了一遍。

一、问题提出

        源表t_source结构如下:

item_id int,

created_time datetime,

modified_time datetime,

item_name varchar(20),

other varchar(20)

1. 源表中有100万条数据,其中有50万created_time和item_name重复。

2. 要把去重后的50万数据写入到目标表。

3. 重复created_time和item_name的多条数据,可以保留任意一条,不做规则限制。

二、实验环境

        Linux虚机:CentOS release 6.4;8G内存;100G机械硬盘;双物理CPU双核,共四个处理器;MySQL 5.6.14。

三、建立测试表和数据

1. 建立源表

create table t_source  
(  
item_id int,  
created_time datetime,  
modified_time datetime,  
item_name varchar(20),  
other varchar(20)  
);  

2. 建立目标表

create table t_target like t_source; 

3. 生成100万测试数据,其中有50万created_time和item_name重复

delimiter //      
create procedure sp_generate_data()    
begin     
    set @i := 1;   
    
    while @i<=500000 do  
        set @created_time := date_add('2017-01-01',interval @i second);  
        set @modified_time := @created_time;  
        set @item_name := concat('a',@i);  
        insert into t_source  
        values (@i,@created_time,@modified_time,@item_name,'other');  
        set @i:=@i+1;    
    end while;  
    commit;    
    
    set @last_insert_id := 500000;  
    insert into t_source  
    select item_id + @last_insert_id,  
           created_time,  
           date_add(modified_time,interval @last_insert_id second),  
           item_name,  
           'other'   
      from t_source;  
    commit;
end     
//      
delimiter ;     
    
call sp_generate_data();  

        源表没有主键或唯一性约束,有可能存在两条完全一样的数据,所以再插入一条记录模拟这种情况。

insert into t_source  
select * from t_source where item_id=1;  
commit;  

        查询总记录数和去重后的记录数图一所示。

select count(*),count(distinct created_time,item_name) from t_source;

图一

        可以看到,源表中有1000001条记录,去重后的目标表应该有500000条记录。

三、无索引对比测试

1. 使用相关子查询

truncate t_target;  
insert into t_target  
select distinct t1.* from t_source t1 where item_id in   
(select min(item_id) from t_source t2 where t1.created_time=t2.created_time and t1.item_name=t2.item_name);  
commit;  

        这个语句很长时间都出不来结果,只看一下执行计划吧。如图二所示,要进行100万*100万次表扫描,难怪出不来结果。

图二

2. 使用表连接查重

truncate t_target;  
insert into t_target  
select distinct t1.* from t_source t1,  
(select min(item_id) item_id,created_time,item_name from t_source group by created_time,item_name) t2  
where t1.item_id = t2.item_id;  
commit;  

这种方法用时35秒,查询计划如图三所示。

图三

(1)内层查询扫描t_source表的100万行,建立临时表,并使用文件排序找出去重后的最小item_id,生成导出表derived2,此导出表有50万行。

(2)MySQL会在临时表derived2上自动创建一个item_id字段的索引auto_key0。

(3)外层查询也要扫描t_source表的100万行数据,在与临时表做链接时,对t_source表每行的item_id,使用auto_key0索引查找临时表中匹配的行,并在此时优化distinct操作,在找到第一个匹配的行后即停止查找同样值的动作。

3. 使用变量

set @a:='0000-00-00 00:00:00';  
set @b:=' ';  
set @f:=0;  
truncate t_target;  
insert into t_target  
select item_id,created_time,modified_time,item_name,other  
  from   
(select t0.*,if(@a=created_time and @b=item_name,@f:=0,@f:=1) f, @a:=created_time,@b:=item_name  
  from   
(select * from t_source order by created_time,item_name) t0) t1 where f=1;  
commit;  

这种方法用时14秒,查询计划如图四所示。

图四

(1)最内层的查询扫描t_source表的100万行,并使用文件排序,生成导出表derived3。

(2)第二层查询要扫描derived3的100万行,生成导出表derived2,完成变量的比较和赋值,并自动创建一个导出列f上的索引auto_key0。

(3)最外层使用auto_key0索引扫描derived2得到去重的结果行。

        与方法2比较,变量方法消除了表关联,查询速度提高了2.7倍。

        至此,我们还没有在源表上创建任何索引。无论使用哪种写法,要查重都需要对created_time和item_name字段进行排序,因此很自然地想到,如果在这两个字段上建立联合索引,可以用于消除filesort,从而提高查询性能。

四、建立created_time和item_name上的联合索引对比测试

1. 建立created_time和item_name字段的联合索引。

create index idx_sort on t_source(created_time,item_name,item_id);  
analyze table t_source;  

2. 使用相关子查询

truncate t_target;  
insert into t_target  
select distinct t1.* from t_source t1 where item_id in   
(select min(item_id) from t_source t2 where t1.created_time=t2.created_time and t1.item_name=t2.item_name);  
commit;  

这种方法用时20秒,查询计划如图五所示。

图五

(1)外层查询的t_source表是驱动表,需要扫描100万行。

(2)对于驱动表每行的item_id,通过idx_sort索引查询出一行数据。

3. 使用表连接查重

truncate t_target;  
insert into t_target  
select distinct t1.* from t_source t1,  
(select min(item_id) item_id,created_time,item_name from t_source group by created_time,item_name) t2  
where t1.item_id = t2.item_id;  
commit;  

这种方法用时25秒,查询计划如图六所示。

图六

        和没有索引相比,子查询虽然从全表扫描变为了全索引扫描,但还是需要扫描100万行记录。因此查询性能提升36%,并不是很多。

4. 使用变量

set @a:='0000-00-00 00:00:00';  
set @b:=' ';  
set @f:=0;  
truncate t_target;  
insert into t_target  
select item_id,created_time,modified_time,item_name,other  
  from   
(select t0.*,if(@a=created_time and @b=item_name,@f:=0,@f:=1) f, @a:=created_time,@b:=item_name  
  from   
(select * from t_source order by created_time,item_name) t0) t1 where f=1;  
commit;

这种方法用时14秒,查询计划与没有索引时的相同,如图四所示。可见索引对这种写法没有作用。能不能消除嵌套,只用一层查询出结果呢?

5. 使用变量,并且消除嵌套查询

set @a:='0000-00-00 00:00:00';  
set @b:=' ';  
truncate t_target;  
insert into t_target  
select * from t_source force index (idx_sort)  
 where (@a!=created_time or @b!=item_name) and (@a:=created_time) is not null and (@b:=item_name) is not null  
 order by created_time,item_name;  
commit; 

这种方法用时8秒,查询计划如图七所示。

图七

        该语句具有以下特点。

(1)消除了嵌套子查询,只需要对t_source表进行一次全索引扫描,查询计划已达最优。

(2)无需distinct二次查重。

(3)变量判断与赋值只出现在where子句中。

(4)利用索引消除了filesort。

        该语句就是吴老师的单线程解决方案。仔细分析这条语句,发现它巧妙地利用了SQL语句的逻辑查询处理步骤和索引特性。

        一条SQL查询的逻辑步骤为:

        步骤1:执行笛卡尔乘积(交叉连接)

        步骤2:应用ON筛选器(连接条件)

        步骤3:添加外部行(outer join)

        步骤4:应用where筛选器

        步骤5:分组

        步骤6:应用cube或rollup

        步骤7:应用having筛选器

        步骤8:处理select列表

        步骤9:应用distinct子句

        步骤10:应用order by子句

        步骤11:应用limit子句

        每条查询语句的逻辑执行步骤都是这11步的子集。拿这条查询语句来说,其执行顺序为:

        强制通过索引idx_sort查找数据行 -> 应用where筛选器 -> 处理select列表 -> 应用order by子句。

        为了使变量能够按照created_time和item_name的排序顺序进行赋值和比较,必须按照索引顺序查找数据行。这里的force index (idx_sort)提示就起到了这个作用,必须这样写才能使整条查重语句成立。否则,因为先扫描表才处理排序,因此不能保证变量赋值的顺序,也就不能确保查询结果的正确性。order by子句同样不可忽略,否则即使有force index提示,MySQL也会使用全表扫描而不是全索引扫描,从而使结果错误。

        索引同时保证了created_time,item_name的顺序,避免了文件排序。force index (idx_sort)提示和order by子句缺一不可,索引idx_sort在这里可谓恰到好处、一举两得。

        查询语句开始前,先给变量初始化为数据中不可能出现的值,然后进入where子句从左向右判断。先比较变量和字段的值,再将本行created_time和item_name的值赋给变量,按created_time,item_name的顺序逐行处理。item_name是字符串类型,(@b:=item_name)不是有效的布尔表达式,因此要写成(@b:=item_name) is not null。

最后补充一句,这里忽略了“insert into t_target select * from t_source group by created_time,item_name;”的写法,因为它受“sql_mode='ONLY_FULL_GROUP_BY'”的限制。

五、总结

        看似一个简单的部分查重语句,要想完美优化,也必须清晰理解很多知识点。如:查询语句的逻辑执行顺序、使用索引优化排序、强制按索引顺序扫描表、索引覆盖、半连接查询优化、布尔表达式等。基础要扎实,应用要灵活,方能书写出高效的SQL语句。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏喔家ArchiSelf

全栈必备JavaScript基础

1995年,诞生了JavaScript语言,那一年,我刚刚从大学毕业。在今年RedMonk 推出的2017 年第一季度编程语言排行榜中,JavaScript 排...

884
来自专栏PhpZendo

需要掌握的 Laravel Eloquent 搜索技术

当我们的应用程序访问较少时(例如在项目初期阶段),直接进行项目编码就可以解决大多数问题。项目中的搜索功能也是如此,没必要在一开始就引入完整的第三方类库进行搜索功...

642
来自专栏惨绿少年

Python入门篇

2214
来自专栏Android 开发者

[译] Kotlin 揭秘:理解并速记 Lambda 语法

在奥地利旅行期间,我参观了维也纳的奥地利国家图书馆。特别是国会大厅,这个令人惊叹的空间感觉就像印第安纳琼斯电影中的一些东西。房间周围的空间是这些门被装在架子上,...

750
来自专栏开源优测

编程入门的姿势-5月8日微信群语音分享

开头语 5月8日在微信群,语音分享了如何如何学习编程语言、并以python为例进行了分享相关经验,下面整理成文章共享给大家。 神马?还有微信群? 加入微信群正确...

3337
来自专栏FreeBuf

Phpcms v9漏洞分析

最近研究源码审计相关知识,会抓起以前开源的CMS漏洞进行研究,昨天偶然看见了这个PHPCMS的漏洞,就准备分析研究一番,最开始本来想直接从源头对代码进行静态分析...

2017
来自专栏IT开发技术与工作效率

Redis 全中文总结

2124
来自专栏python学习路

二、Python介绍

Python 是一门什么样的语言? python是一门动态解释性的强类型定义语言。 编程语言主要从以下几个角度为进行分类,编译型和解释型、静态语言和动态语言、...

2784
来自专栏后端技术探索

浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外),这就意味着PHP本身实现了垃圾回收机制(G...

551
来自专栏施炯的IoT开发专栏

《101 Windows Phone 7 Apps》读书笔记-BABY MILESTONES

课程内容 Ø图片的读写 Ø序列化 Ø双向数据绑定     Baby Milestones将婴儿从出生到2岁之间的发展关键里程碑通知给父母。该应用程序使得父母能...

18110

扫码关注云+社区