本文使用的mysql版本为5.1.48
select sql_no_cache * from one where id< 20 order by id;(id no key)
#0 my_qsort2 (base_ptr=0x1b2176c0, count=4, size=8, cmp=0x869220<ptr_compare_1>,cmp_argument=0x4670ea38) at mf_qsort.c:115 #1 0x00000000008690ea in my_string_ptr_sort (base=0x1b2176c0"Xw!\033", items=4, size=5)at mf_sort.c:37 #2 0x00000000005c12bb in filesort (thd=0x1b1ef980, table=0x1b23a880,sortorder=0x1b23ccf0, s_length=1,select=0x1b23cb50, max_rows=18446744073709551615, sort_positions=false, examined_rows=0x4670ef18) atfilesort.cc:1035 #3 0x000000000053c648 in create_sort_index (thd=0x1b1ef980, join=0x1b2198e0,order=0x1b23bfa0,filesort_limit=18446744073709551615, select_limit=18446744073709551615,is_order_by=false) atsql_select.cc:13832 #4 0x000000000054846a in JOIN::exec (this=0x1b2198e0) at sql_select.cc:2257 #5 0x00000000005492be in mysql_select (thd=0x1b1ef980,rref_pointer_array=0x1b1f1a68, tables=0x1b23b868, wild_num=1, fields=<value optimized out>,conds=0x1b23bd30, og_num=1,order=0x1b23bfa0, group=0x0, having=0x0,proc_param=0x0, select_options=2147764736, result=0x1b23c070, unit=0x1b1f1470,select_lex=0x1b1f1898) at sql_select.cc:2509 #6 0x00000000005497e9 in handle_select (thd=0x1b1ef980, lex=0x1b1f13d0,result=0x1b23c070, setup_tables_done_option=0) at sql_select.cc:269 #7 0x00000000004c97b7 in execute_sqlcom_select (thd=0x1b1ef980,all_tables=0x1b23b868) at sql_parse.cc:5075 #8 0x00000000004d516a in mysql_execute_command (thd=0x1b1ef980) atsql_parse.cc:2271 #9 0x00000000004d5dc4 in mysql_parse (thd=0x1b1ef980, inBuf=0x1b23b650 "select sql_no_cache * from one where id < 20order by id", length=56,found_semicolon=0x46710c30)at sql_parse.cc:5994
通过该bt我们可以知道,filesort是在JOIN的过程中,但又在do_select之前[对于不使用temptabel],使用qsort排序算法。
1. 单表的order by
在JOIN::exec的create_sort_index中进行排序操作,首先它判断是否能够直接通过index获得所需的有序数据(create_sort_index);如果不行的话则进行filesort,即真正的排序过程,在这个函数里,首先调用find_all_keys来获得所有满足条件的数据(通过create_sort_index来test cond),获得的数据首先保存在sort_keys里,这个大小受系统环境变量sort_buff_size的影响,当要排序的数据大于该buff的时候,它们会被write_keys写到tempfile(一个io_cache结构并不一定是一个实体文件),此时它们会先被排完序,即每个io_cache里的数据是有序的。当所有的数据都能够保存到一个sort_keys里(即sort_buff_size足够大),那么它们就会在find_all_keys结束后,由filesort调用save_index进行排序,如果是前一种情况(被分割到多个io_cache)则调用merge_many_buff来完成最终的归并。真正的排序算法(函数)是由my_string_ptr_sort实现的,它又有两种策略:当size <= 20 && items >= 1000 && items <100000(每条记录的大小小于20Byte,并且记录数据[1000,100000])的使用radixsort_for_str_ptr【Radixsort for pointers to fixed length strings. A very quick sort for not to long (< 20char) strings. Neads a extra buffers ofnumber_of_elements pointers but is 2-3 times faster than quicksort】这是源码中给的注释。
对于其它的情况则使用my_qsort2算法,该算法在内容会进行一个判断,当记录数小于THRESHOLD_FOR_INSERT_SORT(默认为10)时使用插入排序,否则就是快速排序。
上面就是排序的整个过程,它们最终被保存到table->sort. record_pointers或者sort.io_cache里。排完序后就是输出。
#0 evaluate_join_record (join=0x1b214c10, join_tab=0x1b23cb78, error=0) atsql_select.cc:11414 #1 0x000000000052dc39 in sub_select (join=0x1b214c10, join_tab=0x1b23cb78,end_of_records=<value optimized out>) at sql_select.cc:11384 #2 0x0000000000542f8a in do_select (join=0x1b214c10, fields=0x1b1f19a0,table=0x0, procedure=0x0) at sql_select.cc:11140 #3 0x0000000000548613 in JOIN::exec (this=0x1b214c10) at sql_select.cc:2314 #4 0x00000000005492be in mysql_select (thd=0x1b1ef980,rref_pointer_array=0x1b1f1a68, tables=0x1b2146e0, wild_num=1, fields=<valueoptimized out>, conds=0x0, og_num=1, order=0x1b214b20, group=0x0,having=0x0, proc_param=0x0, select_options=2147764736, result=0x1b214bf0,unit=0x1b1f1470, select_lex=0x1b1f1898) at sql_select.cc:2509
从该bt可以看出在order by之后(2257)2314,调用了我们在MYSQLJOIN过程里分析的do_select操作,此时只有一层的sub_select,evaluate_join_record,因为这里是单表。这里sub_select获得的数据不再是从plugin里去读而是调用rr_unpack_from_buffer;rr_unpack_from_tempfile(Read aresult set record from a buffer after sorting)从record_pointers或io_cache中读,而且在evaluate_join_record里也不会再去test(select_cond->val_int()),因为select_cond,现在被置为空[这些操作都是在filesort之后赋值的],最后evaluate_join_record调用end_send发送给客户端。
2. 两个表的join,并且order by为驱动表
explain select * from titles t joinemployees e on t.emp_no=e.emp_no where t.title='Engineer' order bye.first_name;
对于这种情况,其实进行的操作跟一个表的是一样的,只是最后在send data的执行do_select的时候(sql_select.cc:2314),此时就是真正的两个表进行join了,也就是它是两层的sub_select----evaluate_join_record。只是对于驱动表在sub_select的时候是从sort.record_pointers或者sort.io_cache获得数据的,其它的就与我们介绍MYSQL JOIN过程的一样了。
3. 两个表的join,并且order by为非驱动表
explain select * from titles t joinemployees e on t.emp_no=e.emp_no where t.title='Engineer' order by t.from_date;
与上图相比这里多了一个using temporary[网上有文章说为什么这个显示在第一行,其实它显示在哪一行都不合适,因为using temporary,using filesort都是在两者join之后],也就是说以对于这种情况需要一个temporary table来辅助实现,下面我们来分析一下这个temp tab是什么作用。这里的代码如下:
#0 sub_select (join=0x1b2270a0, join_tab=0x1b21a618,end_of_records=<value optimized out>) at sql_select.cc:11401 #1 0x000000000052da0e in evaluate_join_record (join=0x1b2270a0,join_tab=0x1b21a3c0, error=<value optimized out>) at sql_select.cc:11511 #2 0x000000000052dc39 in sub_select (join=0x1b2270a0, join_tab=0x1b21a3c0, end_of_records=<value optimized out>) at sql_select.cc:11384 #3 0x0000000000542f8a in do_select (join=0x1b2270a0, fields=0x0,table=0x1b23d670, procedure=0x0) atsql_select.cc:11140 #4 0x0000000000546e8c in JOIN::exec (this=0x1b2270a0) at sql_select.cc:1907 #5 0x00000000005492be in mysql_select (thd=0x1b1ef980,rref_pointer_array=0x1b1f1a68, tables=0x1b23b900, wild_num=1, fields=<value optimized out>,conds=0x1b23c978, og_num=1,order=0x1b23cc18, group=0x0, having=0x0,proc_param=0x0, select_options=2147764736, result=0x1b23ccf0, unit=0x1b1f1470,select_lex=0x1b1f1898) at sql_select.cc:2509
可以看出这里进行do_select(1907)操作是在2257:create_sort_index()之前,这与我们之前的join文档分析的过程不一样,即join的过程提前了,因为在前面有一个if (need_tmp)判断,这个need_tmp就是为order或group使用。并且当join到一条满足条件的语句时执行的join_tab->next_select为:end_write[两层evaluate_join_recordtest(select_cond->val_int())后]。而这时写的位置为join->tmp_table;它的初始化为do_select的开头,即把进入do_select时指定的need_tmp的temp_table赋值给它。即把join后的所有记录保存到一个temp table里[join->tmp_table]。
接下来执行change_to_use_tmp_fields,就是让order by使用temp fields。并把temp table的信息拷贝到join里。
再执行create_sort_index,执行sort操作,因为现在传给filesort的select为空,所以在find_all_keys的时候不会再进行cond判断。(即select->skip_record不会被执行)最后再send data,此时还会执行一次do_select【如果没有tmp_table的话就会只执行一次这个do_select,如单个表的order by】,此时join只有一个temp table,所以这里只有一层sub_select与evaluate_join_record。最后的join_tab->next_select为end_send。
4. 为什么MYSQL认为使用temp table会更优?
首先我们看几个数据:
select count(*) from employees:300024
select count(*) from titles:443308
select count(*) from titles wheretitle='Engineer':115003
e表比t表小,所以它全表扫描的时候会快一点,但是t表有一个where条件经过test cond之后只剩下115003了,所以按这个数据看MS使用t表去join e表可能更快。而且前者还使用了temp table,实在是想不明白为什么MYSQL会这样做,所以就通过如下实验对比一下两者的性能。
A:explain select sql_no_cache * from titlest JOIN employees e on t.emp_no=e.emp_no wheret.title='Engineer' order by t.from_date;
执行结果:115003 rows in set (3.09 sec)
B:explain select sql_no_cache * from titlest STRAIGHT_JOIN employees e on t.emp_no=e.emp_nowhere t.title='Engineer' order by t.from_date;
执行结果:115003 rows in set (1.40 sec)
实验结果如同我们上面分析的:使用t作为驱动表比使用e(MYSQL默认的优化结果)性能更好。
其实这个对于不使用order by的情况应该是一样的,所以下面我们又实验了这两种情况:
select sql_no_cache * from titles t JOIN employees e on t.emp_no=e.emp_no wheret.title='Engineer';
115003 rows in set (2.50 sec)
select sql_no_cache * from titles t STRAIGHT_JOIN employees e on t.emp_no=e.emp_no wheret.title='Engineer';
执行结果:115003 rows in set (1.05 sec)
通过上面的实验我们可以看到MYSQL的优化其实并不一定是可靠的,一个原因可能是它无法去判断where cond后的数据信息。而这些信息对于join操作是可能有较大的影响。
5. 总结
整个过程可以简单的描述为如果能够对单个表进行order的话就直接先对它的过滤集进行order by操作。如果不能,即会在join后才进行order by,此时会产生一个中间的temporary table做为最后的order by对象(也可以这样说order by只会对一个表进行操作,只是这个表可能就是本身存在的一个实体表或者是由多个表join后产生的一个临时表)。另外排序算法还有两种选择策略:一种是只取出要order的字段与一个可以返回一个记录的指针,对这个把它们放到sort_buff中排序,排完后再按顺序取出每个记录的相应字段;另一种是把order字段以及所有要返回的字段都一次性取出。这两种策略的选择是由系统环境变量max_length_for_sort_data(get_addon_fields)决定的,当要取得的字段类型总和大于max_length_for_sort_data时就使用第一种,否则使用第二种。显然第一种需要多一次IO,但内存使用更少。所以当order by性能差的时候可以考虑适当的提高max_length_for_sort_data,或者sort_buffer_size,前者减少IO,后者减少meger。
参考文献:
简朝阳:http://isky000.com/database/mysql_order_by_implement
http://www.cnblogs.com/phper7/archive/2010/05/26/1744062.html