groupby执行流程
GROUP BY 的执行流程可以分为语法解析Parser、优化器优化Optimizer和执行器执行Executor三大阶段。下面我将为你详细拆解每个阶段的具体操作。第一阶段语法解析与语义分析 (Parser Analyzer)这个阶段是SQL语句进入数据库引擎的第一步主要任务是“理解”这条SQL在说什么。1. 词法与语法解析数据库首先会将你输入的SQL文本字符串解析成一个内部的解析树Parse Tree。解析器会检查SQL的语法是否正确比如 GROUP BY 关键字是否写对括号是否匹配等。2. 语义分析与查询重写解析树生成后系统会进行语义检查确保引用的表、列等对象是真实存在的并且你有权限访问它们。3. 生成查询树Query Tree在这个阶段系统还会进行一些逻辑上的查询重写。例如DISTINCT 在某些数据库如MySQL的优化器眼中可能会被等价改写为 GROUP BY 操作。对于 SELECT COUNT(*) FROM t 这种没有 GROUP BY 的查询也会被识别为一种特殊的“标量分组”SCALAR GROUP BY操作。至此一个结构化的、可被优化器处理的**查询树Query Tree就准备好了。第二阶段优化器优化 (Optimizer)这是 GROUP BY 执行流程中最核心、最关键的阶段。优化器的目标是在成百上千种可能的执行路径中选择一条代价Cost最小的计划。对于 GROUP BY优化器主要进行以下操作1. 识别分组操作优化器识别出查询树中的 GROUP BY 子句并明确需要进行分组聚合的列。2. 代价评估与执行策略选择这是优化的核心。优化器会基于表统计信息如数据量、索引分布等评估不同执行策略的代价主要分为两类2.1. 索引分组 (Index-based Grouping)如果分组列是某个索引的最左前缀且数据本身就是按该索引顺序存储的如B-Tree索引优化器会优先考虑利用索引。2.2.1. 松散索引扫描 (Loose Index Scan)这是最高效的方式。MySQL可以直接利用索引的有序性跳跃式地读取每个分组的第一行或最后一行来完成分组无需访问所有索引项。例如对 (c1, c2, c3) 索引执行 GROUP BY c1, c2 就可能使用此方法。2.1.2. 紧凑索引扫描 (Tight Index Scan)**如果无法使用松散扫描MySQL也可能通过遍历整个索引范围来完成分组。2.2. 临时表分组 (Temporary Table Grouping)如果无法有效利用索引优化器将选择创建临时表。它会根据代价估算在**哈希分组 (HASH GROUP BY)** 和**排序分组 (MERGE/SORT GROUP BY) 两种算法中做出选择。2.2.1. 哈希分组在内存中构建一个哈希表以分组列为键聚合结果为值。适合分组数量不多的场景。2.2.2. 排序分组先对数据按分组列排序然后顺序扫描将相同键值的行归为一组进行聚合。适合大量数据或内存不足的场景。3. 生成执行计划经过代价评估优化器最终会生成一个包含具体操作步骤如 Table Scan, Hash Group By, Sort, Index Scan 等的**执行计划Execution Plan**并将其传递给执行器。第三阶段执行器执行 (Executor)执行器会按照优化器生成的执行计划一步步地真正去获取和处理数据。我们以最常见的、无法使用索引的临时表方案为例看看执行阶段的具体操作。1. 创建临时表执行器首先在内存或磁盘如果数据量过大中创建一个临时表。这个临时表的结构包含 GROUP BY 的列和各个聚合函数的列。例如对于 SELECT city, COUNT(*) FROM staff GROUP BY city;临时表会有 city 和 num 两个字段。2. 全表/索引扫描与填充执行器开始扫描源表或通过索引访问的数据行。对于每一行它会取出 GROUP BY 列的值如 city。然后它会检查临时表中是否已存在该分组键值的记录。如果不存在在临时表中插入一条新记录聚合函数初始化为对应的值如 num 设为1。如果已存在更新该记录执行聚合操作如 num 加1。3. 可选排序当所有数据行处理完毕后如果 GROUP BY 本身隐含了排序需求大多数数据库默认会按分组列排序或者查询中有 ORDER BY 子句执行器需要对临时表进行排序。这个过程通常被称为 Using filesort。4. 可选HAVING过滤如果存在 HAVING 子句执行器会在分组和聚合完成后对每个分组应用 HAVING 条件过滤掉不满足条件的分组。5. 返回结果最后执行器将临时表中的结果集返回给客户端。总结下图概括了 GROUP BY 的完整执行流程理解这个流程对于优化 GROUP BY 查询至关重要。其核心思想就是帮助优化器选择最高效的执行路径而绝大多数情况下为 GROUP BY 的列创建合适的索引是避免昂贵的“临时表排序”操作、提升查询性能最有效的手段。举个例子说明:假设我们有这样一张销售表 sales里面存了5条简单数据且 product_category 字段上没有索引idproduct_categoryamount1电子产品2002书籍503电子产品1504书籍305电子产品100我们执行这条 SQLSELECTproduct_category,SUM(amount) AS total_sales,COUNT(*) AS order_countFROM salesGROUP BY product_categoryHAVING COUNT(*) 1;第一阶段语法解析Parser Analyzer——“读懂句子”数据库拿到这段字符串后先当“语文题”做词法语法检查检查 SELECT、FROM、GROUP BY、HAVING 拼写对不对括号和引号是否匹配。语义检查去元数据表查一下确认库里有 sales 这张表且表里确实有 product_category 和 amount 这两列。确认你拥有查询权限。生成查询树将SQL结构化生成一棵逻辑树。在此阶段解析器会将 HAVING COUNT(*) 1 识别为分组后的过滤条件并把它和 GROUP BY 绑定在一起。此时数据还没动只是把“干什么”翻译成了数据库内部能看懂的逻辑指令树。第二阶段优化器优化Optimizer——“制定作战计划”这是最精彩的部分。优化器知道我们要按 product_category 分组但它发现 这张列上没有索引。策略排除因为没索引优化器直接放弃了“索引分组Index-based Grouping”这条路。代价估算Cost-Based它统计到这张表只有 5 行数据非常小。优化器在“哈希分组HASH GROUP BY”和“排序分组SORT GROUP BY”之间做选择。因为数据量极小且大多数数据库如 MySQL 5.7默认倾向于在内存中建立哈希表来分组这样不需要额外的磁盘排序代价更低。于是优化器决定采用 临时表Temporary Table 哈希分组Hash Grouping 的策略。生成执行计划优化器最终输出的执行计划你可以用 EXPLAIN 看到大致是Step 1: 全表扫描Table scan on salesStep 2: 创建内存临时表Create temporary table以 product_category 为 KeyStep 3: 使用哈希聚合Hash aggregate填充临时表Step 4: 对临时表执行 HAVING 过滤Step 5: 返回结果因为 GROUP BY 默认不保证顺序这里没 ORDER BY所以不排序直接返回注意假如我们给 product_category 建了索引优化器就会改用“松散索引扫描Loose Index Scan”直接利用索引的有序性跳过重复值完全跳过下面昂贵的临时表创建步骤。第三阶段执行器执行Executor——“真正动手干活”执行器严格按照优化器给的计划开始逐行处理数据。我们一起来跟踪内存临时表的变化执行器在内存中建了一张临时表结构是这样的(category VARCHAR, sum_amount INT, count_rows INT)。第1行id1电子产品200拿着“电子产品”去临时表查找不到。插入新行(电子产品, 200, 1)product_categorytotal_sales (SUM)order_count (COUNT)电子产品2001第2行id2书籍50拿着“书籍”去查找不到。插入新行(书籍, 50, 1)product_categorytotal_salesorder_count电子产品2001书籍501第3行id3电子产品150拿着“电子产品”去查找到了更新该行SUM 200 150 350COUNT 1 1 2product_categorytotal_salesorder_count电子产品3502书籍501第4行id4书籍30找到了“书籍”更新SUM 50 30 80COUNT 1 1 2product_categorytotal_salesorder_count电子产品3502书籍802第5行id5电子产品100找到了“电子产品”更新SUM 350 100 450COUNT 2 1 3product_categorytotal_salesorder_count电子产品4503书籍802最后两步HAVING 过滤与结果返回执行 HAVING 过滤执行器遍历完所有原始数据后临时表已经存好了分组结果。现在轮到 HAVING COUNT(*) 1 出场了检查“电子产品”order_count 3满足 1保留。检查“书籍”order_count 2满足 1保留。返回结果执行器将临时表中的两行数据直接发送给客户端。最终你看到的查询结果就是product_categorytotal_salesorder_count电子产品4503书籍802总结与关键知识点通过这个例子你能直观感受到解析阶段只管“合法性”和“读懂”SQL。优化阶段因为没索引主动选择了“临时表哈希聚合”避开了昂贵的磁盘排序。执行阶段临时表在内存中不断被查找哈希查找和更新这个过程非常快5行数据但如果换作 500 万行数据且内存不够执行器就会把临时表转为基于磁盘的 On-Disk Temporary Table并使用排序分组Sort Group By算法性能会急剧下降。举个反例加深理解如果我们在 product_category 上建了索引 CREATE INDEX idx_category ON sales(product_category);那么优化阶段就会判定“索引分组”代价更低执行器便会直接利用索引的有序性按顺序读取 电子产品, 电子产品, 电子产品, 书籍, 书籍每切换一个类别就立即计算 SUM 和 COUNT完全不需要创建临时表这就是 GROUP BY 性能优化的核心底层原理。