ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

13-数据库学习笔记(查询执行)

13-数据库学习笔记(查询执行) 一.查询执行基础概述1.为什么SQL不能直接执行因为SQL是一种声明式语言只告诉数据库我要什么结果但不说“具体怎么找”数据库是无法执行查询操作的所以我们必须把SQL翻译成一张施工图纸这张图纸就叫做查询计划。整个查询步骤可以理解成用户写一条 SQL → 数据库分析它 → 制定执行方案 → 调用底层算子处理数据 → 返回结果查询计划长得像一棵倒过来的树DAG 有向无环图由一个个算子封装了特定数据操作逻辑的数据处理执行单元搭成。数据从叶子往上流最顶上根节点输出的就是最终看到的结果。2.流水线与阻断算子流水线数据像流水一样从一个算子流向下一个算子。来一行、处理一行、输出一行不用攒数据边输入边输出。阻断算子必须等前面所有数据处理完才能继续向后输出结果。这样设计的好处与代价优点树状结构分工清楚、算子能复用流水线让数据流着走不用反复落地存盘又快又省缺点一旦碰上排序、连接这类阻断算子流水线就得暂停、还得腾出内存先存数据整体就慢下来了二.数据库查询处理模型查询处理模型 数据库执行 SQL 时各个算子之间如何传递数据、如何调用、如何运行。1.为什么需要“处理模型”不同业务场景数据库负载差异极大OLTP事务型业务追求低延迟、少量数据读写OLAP分析型业务适配海量数据批量扫描。对于不同的场景数据库实际干的活完全不一样没法用单一模式适配这两种场景于是业界迭代出了三类主流处理模型迭代器 / 物化 / 向量化2.处理模型核心逻辑(控制流数据流)处理模型定义了查询计划的执行方式与数据流动机制其核心由控制流和数据流两部分构成控制流决定算子的调用与调度方式数据流决定数据在算子间的传递模式。3.迭代器模型火山模型迭代器模型是关系数据库查询执行引擎的经典架构因为数据从底层逐层向上流动、像火山喷发一样也被称为火山模型。它的核心设计思想是所有算子都实现一套统一的迭代器接口上层算子只需要调用下层算子的接口就能逐行获取处理好的数据。整个执行计划就是一棵由迭代器算子组成的树通过递归调用完成数据处理。所有算子统一实现Open()、Next()、Close()三类函数Open()初始化算子资源Next()单次调取一条元组无数据时返回结束标记Close()释放资源1执行流程初始化阶段从根节点开始递归调用open()一直传递到最底层的扫描算子完成所有资源初始化。数据处理阶段从根节点开始调用next()递归向下层要数据下层处理完一行就向上层返回一行层层处理最终从根节点输出一行结果。结束阶段当next()返回空时从根节点递归调用close()逐层释放资源。2例子示例SQLSELECTname,salaryFROMemployeesWHEREdepartment研发部ORDERBYsalaryDESC;执行计划树Sort(salary DESC) —— 阻断算子根节点 └── Project(name, salary) —— 流水线算子 └── Select(department研发部) —— 流水线算子 └── SeqScan(employees) —— 流水线算子叶子节点原始数据idnamedepartmentsalary1张三研发部180002李四市场部150003王五研发部220004赵六研发部160005孙七人事部130006周八研发部20000执行全过程1. 初始化递归open()从根节点自上而下调用open()逐层初始化资源最终扫描算子打开表文件行指针指向第 1 行。2. 核心迭代递归next()客户端反复调用根节点next()每次获取一行结果。第 1 次调用Sort.next()阻断逻辑触发Sort 是阻断算子必须先拿到全部数据才能输出。它循环调用下层拉取数据数据沿SeqScan → Select → Project逐行流动扫描读一行 → 过滤部门 → 提取目标列符合条件的行存入 Sort 内部缓冲区。扫描到文件末尾返回null后Sort 对 4 条有效数据执行全量降序排序王五(22000) → 周八(20000) → 张三(18000) → 赵六(16000)返回排序后的第 1 行给客户端。第 2~4 次调用Sort.next()直接从内部缓冲区按指针逐行返回无需再访问下层算子。第 5 次调用Sort.next()缓冲区读取完毕返回null查询结束。3. 收尾递归close()从根节点自上而下调用close()逐层释放资源、关闭文件。3优缺点优点1.简单统一所有算子接口统一 2.支持流水线 3.节省内存不用保存大量中间结果缺点频繁进行函数调用。开销大4.物化模型1执行流程物化模型每个算子把自己的全部结果计算出来保存下来再交给下一个算子。也就是先算完 → 存起来 → 再传给下一层。对比迭代器的「逐行流动」物化模型的核心是每个算子一次性处理完所有输入生成完整的中间结果集再整体交付给上层算子。每层都是 “全量输入→全量输出”没有逐行迭代的过程。2例子第 1 步全表扫描 → 物化全量原始数据SeqScan 算子一次性读取 employees 表全部 6 行数据生成中间结果集 R1完整原始表整体交付给 Select 算子。中间结果 R1全部 6 行原始数据第 2 步选择过滤 → 物化过滤后结果Select 算子拿到完整的 R1 后一次性对所有 6 行执行条件过滤保留研发部的 4 行生成中间结果集 R2整体交付给 Project 算子。中间结果 R24 行符合条件的完整数据张三、王五、赵六、周八第 3 步列投影 → 物化简选列结果Project 算子拿到完整的 R2 后一次性去掉多余列只保留 name 和 salary 两列生成中间结果集 R3整体交付给 Sort 算子。中间结果 R34 行两列数据 —— (张三18000)、(王五22000)、(赵六16000)、(周八20000)第 4 步排序 → 物化最终结果Sort 算子拿到完整的 R3 后一次性执行全量降序排序生成最终结果集 R4整体返回给客户端。最终结果 R4王五 (22000) → 周八 (20000) → 张三 (18000) → 赵六 (16000)3核心特点全量交付无逐行交互算子之间只有一次完整结果的交付没有反复的next()调用不存在行级流水线。所有算子本质都是阻断式每个算子都必须等下层全部做完才能开始工作没有流水线算子与阻断算子的区分。内存占用高每一层都会产生一份完整的物化中间结果n 层算子就会有 n 份全量数据副本数据量大时内存压力极大。结果返回延迟高必须等从最底层到最顶层所有算子全部执行完才能返回结果无法像迭代器模型那样边执行边输出第一行。5.向量化模型1执行流程向量化执行 不再一次处理一条记录而是一次处理一批记录利用CPU高速计算能力提升查询性能。2例子1. 初始化递归open()与迭代器模型逻辑一致自上而下递归初始化每层算子分配批次缓冲区用于存放一批列向量数据。2. 核心迭代逐批拉取与处理客户端反复调用根节点next()每次获取一批结果而非单行。第 1 次调用Sort.next()批级阻断触发Sort 是阻断算子需收集全部批次数据后排序输出。它循环调用下层算子拉取批次第 1 批数据流转SeqScan.next () → 读取前 2 行返回列向量批次name[张三, 李四]department[研发部, 市场部]salary[18000, 15000]Select.next () → 批量判断 department 向量一次性过滤掉不符合条件的李四输出过滤后批次name[张三]department[研发部]salary[18000]Project.next () → 批量剔除 department 列输出投影后批次name[张三]salary[18000]Sort 将该批数据存入全局缓冲区。第 2 批数据流转SeqScan.next () → 读取中间 2 行返回批次name[王五, 赵六]department[研发部, 研发部]salary[22000, 16000]Select 批量过滤全部符合→ Project 批量投影 → Sort 存入缓冲区。第 3 批数据流转SeqScan.next () → 读取最后 2 行返回批次name[孙七, 周八]department[人事部, 研发部]salary[13000, 20000]Select 过滤掉孙七 → Project 投影 → Sort 存入缓冲区。全量排序SeqScan.next () 返回空批次Sort 确认数据拉取完毕。此时缓冲区共 4 行数据一次性执行全量降序排序得到最终有序结果王五(22000) → 周八(20000) → 张三(18000) → 赵六(16000)排序完成后Sort 按批次大小拆分结果返回第 1 批name[王五, 周八]salary[22000, 20000]第 2 次调用Sort.next()直接从排序后的结果缓冲区返回第 2 批name[张三, 赵六]salary[18000, 16000]第 3 次调用Sort.next()缓冲区读取完毕返回空批次查询结束。3. 收尾递归close()自上而下逐层释放资源清空批次缓冲区关闭数据文件。3优缺点优点大幅减少函数调用次数适配海量数据扫描紧凑循环处理数组编译器可深度优化CPU缓存利用率高。缺点小批量轻量查询存在批次等待开销延迟高于迭代器模型。三.查询计划执行方向查询计划的数据流方向和算子的调用方向。1.自上而下拉取式火山模型数据向上调用向下假设有一个执行计划用户想要获取一个结果于是最上面的Project会向下依次传话“给我一个结果”。于是Project.next()→Filter.next()→Scan.next()。由此我们可以看到调用方向是Project→Filter→Scan也就是从上往下调用。而数据返回时用户←Project←Filter←Scan也就是从下往上传递。所以自上而下的特点就是控制流向下数据流向上。优点接口简单算子接口统一易控制受上层决定。缺点频繁调用函数开销大2.自下而上推送式物化模型计算自底而上主动交给上层整体流程Scan→生成中间结果T1→Filter→生成中间结果T2→Project→最终结果这里整体就是从下往上计算。当下层的数据产生后会主动向上推给上层。优点缓存寄存器利用率高。缺点中间结果难管控四.数据访问方式查询执行器负责“怎么算”数据访问方式负责“去哪找数据怎么取数据”例如如下SQLSELECT*FROMStudentWHEREid100;数据库有很多种办法找到这条记录从第一条扫描到最后一条利用索引直接定位利用哈希快速查找这就是数据访问方式。数据访问整体流程SQL→查询优化器→查询执行计划→执行算子→数据访问方式→内存池→页→磁盘1.顺序扫描顺序扫描是最基础的数据读取方式也叫全表扫描。从一个表的第一页开始逐页检查遍历每个页面的全部元组。优点无索引依赖稳定性强缺点全表扫描IO开销大海量数据查询效率极低。2.索引扫描不直接搜索表而是先搜索索引。依据查询条件、索引属性筛选最优索引优先选择筛选度高、过滤冗余数据多的索引减少磁盘读取行数精准定位目标元组。优点精准过滤数据IO开销极低适配高筛选率条件查询。缺点索引存在存储维护开销低筛选率场景下性能不如顺序扫描。3.哈希访问利用哈希key直接定位直接查找优点查询非常快五.万圣节问题万圣节问题是数据库执行更新操作时由于更新改变了数据的位置或索引结构使得已经访问过的数据重新被扫描从而导致重复更新甚至无限更新的问题。解决方案UPDATE 算子维护一个已修改元组 ID 的集合记录集每当一个元组被处理就把它的 ID 记录下来后续扫描再次遇到相同 ID 时直接跳过不再重复更新。优点使用ID追踪访问状态简单高效规避了重复修改异常保障了事务数据一致性。缺点需要记录修改ID轻微占用内存空间。
返回列表