可以看出, 在产生执行计划过程中crash了。

堆栈表明, 函数中join_tab->join->join_list为无效地址。 排查看到函数入口处这个变量还是ok的, 那么在gdb里watch一下。

  1. Hardware watchpoint 4: join_tab->join->join_list
  2. Old value = (List<TABLE_LIST> *) 0x3431f60
  3. New value = (List<TABLE_LIST> *) 0xc800000000000000

这么整齐的地址一看就有问题。函数栈:

  1. Key_field::Key_field
  2. add_key_field
  3. add_key_equal_fields
  4. add_key_fields
  5. update_ref_and_keys

add_key_fields修改join_tab->join->join_list实际是不合理的, 因此这里说明一下路径上几个关键的函数。

还要从子查询优化说起,当遇到semi-join子查询情况下, JOIN::optimize()会调用JOIN::flatten_subqueries改写SQL, 如下形式:

  1. SELECT ...
  2. FROM ot SEMI JOIN (it1, ... , itN),

函数JOIN::flatten_subqueries, 做了以下几件事:

  • 创建semi join(it1, …, itN)的节点并添加到外层查询语句的FROM语法树下
  • subq_where AND oe=ie加入到外层查询语句的WHERE树下
  • 再移除原先的子查询语句

中, 对于每一个子查询, 调用函数JOIN::convert_subquery_to_semijoin, 那么子查询上维护的query信息也要同步加到外部查询上。所以可见, 子查询中的信息, 会转交给外部查询。

之后, JOIN::optimize()调用update_ref_and_keys, 这个函数用来处理出最终查询要使用的索引。crash的问题也出现在这个函数中, 因此还要看update_ref_and_keys内部做了什么。

在函数update_ref_and_keys中, 一个重要的数组, key_fields, 用来存放所有可能用到的索引字段。先通过key_fields=(Key_field*) thd->alloc(sz)分配空间, 再调用add_key_fields递归遍历WHERE树, 遇到等值表达式, 会填充到key_fields数组中。而之前已经看到, add_key_field在写key_fields时却修改了join_tab->join->join_list

  1. // add_key_fields中修改了join_tab->join->join_list的代码
  2. new (*key_fields)
  3. Key_field(field, *value, and_level, exists_optimize, eq_func,
  4. null_rejecting, NULL, get_semi_join_select_list_index(field));
  5. (*key_fields)++;

可见在new的时候拿到了join_tab->join->join_list, 是(*key_fields++)的时候, 加过头了。从而可推断, key_fields没有分配到应该有的内存空间。那么出问题的就是sz用来分配空间的数字了。

函数JOIN::convert_subquery_to_semijoin中, 改写完SQL后, 忘记把子查询的cond_count和between_cond信息更新到外部查询了, 这时只要手动添加即可。

官方修复(5.6.25)参见

可以通过以下方式复现

  1. create table t1 (
  2. `col3` varchar(60) default null,
  3. primary key (id)
  4. );
  5. select t1.col1 as col1,
  6. t2.col2 as col2,
  7. ...
  8. t30.col30 as col30
  9. from (((((((((((((((((((((((((((((t1
  10. left join t2 on (t1.id = t2.id))
  11. left join t3 on (t1.id = t3.id))
  12. ...
  13. left join t30 on (t1.id = t30.id));

然后执行

  1. create table tt (id int(20), b varchar(200));

MySQL5.6在5.6.25之前的小版本都可以复现, 请尽情调戏 .^.