带你读 MySQL 源码:Select *


带你读 MySQL 源码:Select *

文章插图
1、整体介绍对于 select * from table 中的星号,我们再熟悉不过了:它告诉 MySQL 返回表所有字段的内容 。
MySQL 服务端收到 select 语句之后,会在 server 层把星号展开为表中的所有字段,然后告诉存储引擎返回这些字段的内容 。
对于存储引擎来说,它只需要按照 server 层的要求返回指定字段的内容即可,它不知道(也不需要知道)客户端是要求返回表中所有字段,还是部分字段的内容 。
select * 中的星号展开为表中所有字段涉及 2 个阶段:
  • 词法 & 语法分析阶段:标记 select 字段列表中包含几个星号 。
  • 查询准备阶段:把星号展开为表中所有字段 。
2、源码分析(1)Item_asterisk::itemize()// sql/item.ccbool Item_asterisk::itemize(Parse_context *pc, Item **res) {...pc->select->with_wild++;return false;}多表连接时,select 字段列表中可能会包含多个星号,词法 & 语法分析阶段,每碰到 select 字段列表中的一个星号,Item_asterisk::itemize() 就会给 pc->select->with_wild 属性加 1 。
pc->select 是 Query_block 对象的指针,定义如下:
 // sql/parse_tree_node_base.hstruct Parse_context {...Query_block *select; ///< Current Query_block object...};后面 Query_block::prepare() 访问的 with_wild 属性就是这里的 pc->select->with_wild 。
(2)Query_block::prepare()// sql/sql_resolver.ccbool Query_block::prepare(THD *thd, mem_root_deque<Item *> *insert_field_list) {...if (with_wild && setup_wild(thd)) return true;...}prepare() 方法中,关于 select * 的逻辑比较简单,就这一行 。
如果 with_wild 大于 0,则调用 setup_wild(thd),处理 select 字段列表中星号展开为表中所有字段的逻辑 。
(3)Query_block::setup_wild() // sql/sql_resolver.ccbool Query_block::setup_wild(THD *thd) {...// 从 select 字段列表中的第 1 个字段开始处理// 满足 2 个条件中的任意一个就结束循环:// 1. with_wild > 0 为 false,//说明已处理完所有星号,结束循环// 2. it != fields.end() 为 false,//说明已经处理了所有字段,结束循环for (auto it = fields.begin(); with_wild > 0 && it != fields.end(); ++it) {Item *item = *it;// item->hidden = true// 表示 select 字段列表中的这个字段// 是查询优化器给【偷偷】加上的// 肯定不会是星号,直接跳过if (item->hidden) continue;Item_field *item_field;// Item::FIELD_ITEM 说明当前循环的字段// 是个普通字段,不是函数、子查询等// 那它就有可能是星号,需要通过 item_field->is_asterisk()// 进一步判断是否是星号if (item->type() == Item::FIELD_ITEM &&(item_field = down_cast<Item_field *>(item)) &&// 如果 item_field 对应的字段是星号// item_field->is_asterisk() 会返回 trueitem_field->is_asterisk()) {assert(item_field->field == nullptr);// 只有 create view as ... 中的 select 语句// any_privileges 为 true// 其它情况下,它的值为 false// insert_fields() 方法中会用到const bool any_privileges = item_field->any_privileges;// 如果当前 Query_block 对应的是子查询// master_query_expression()->item// 指向主查询中该子查询所属的 where 条件Item_subselect *subsel = master_query_expression()->item;...// 当前 Query_block 是 exists 子查询// 并且子查询中不包含 having 子句// 则可以把子查询中的星号替换为常量if (subsel && subsel->substype() == Item_subselect::EXISTS_SUBS &&!having_cond()) {...*it = new Item_int(NAME_STRING("Not_used"), 1,MY_INT64_NUM_DECIMAL_DIGITS);} else {// 不满足 if 中的条件// 则需要调用 insert_fields()// 把星号展开为表中所有字段assert(item_field->context == &this->context);if (insert_fields(thd, this, item_field->db_name,item_field->table_name, &fields, &it, any_privileges))return true;}// 每处理完 select 字段列表中的一个星号// with_wild 就减 1// 减到 0 之后,就说明所有星号都已经处理过了with_wild--;}}return false;}Query_block::setup_wild() 的主体逻辑是迭代 select 字段列表中的每个字段,遇到星号就处理,不是星号就忽略,星号的处理逻辑有 2 种:
第 1 种:满足 if (subsel && ...) 条件,说明 select 语句是 where 条件中的 exists 子查询,并且子查询中不包含 having 子句 。这种场景下,select 字段列表中的星号可以被替换为常量,而不需要展开为表的所有字段 。
*it = new Item_int(...)? 创建了一个代表常量的字段对象,字段名为 Not_used?,字段值为 1,用于替换 select 字段列表中的星号 。


推荐阅读