0%

摘要

该论文解决了两个问题:

  1. 如何将记录迁移到冷存储以及从冷存储迁出。
  2. 如何以事务一致的方式读取和更新冷存储中的记录。

论文提出了三个新颖的想法,并详细阐述了每个想法的实现方法:

  1. 统一的接口,向高层隐藏记录的物理位置:通过冷存储、访问过滤器、私有缓存和更新备忘录之间的协作实现。
  2. 最小化访问二级存储带来的开销:每个事务拥有自己的私有缓存。
  3. 热存储与冷存储之间的无缝迁移:系统在事务中使用插入和删除操作执行迁移。

分析与实验结果:论文在 YCSB 基准测试和多步骤读/更新工作负载上评估了 Siberia 的性能,得出以下结论。在现实的客户端延迟下,吞吐量损失仅为 3%。即使在极高的冷数据访问率下,内存中的 Siberia 机制也仅带来较低的性能损失。当实时迁移激活时,系统性能保持稳定。访问备忘录的开销较大,这意味着备忘录清理对于提高性能很重要。在冷数据访问率为 5% 和 10% 的现实场景下,只读事务的性能损失分别为 7% 和 14%,这是可接受的。对于纯更新事务,5% 的冷数据更新导致 8% 的吞吐量损失,10% 的冷数据更新率导致 13% 的吞吐量损失,同样可接受。在 YCSB 工作负载下,随着访问偏斜度降低以及内存与数据库大小比例增加,性能会下降;与写密集型工作负载相比,读密集型工作负载在更高的偏斜率下表现出更低的事务中止率。

论文优势

  • 论文提供了全面的文献综述。在 HEKATON 存储与索引 部分,论文简要概述了 HEKATON 的索引和数据存储结构及其基于 MVCC(多版本并发控制)的读写操作。在 RELATED WORK 部分,论文分析了现有数据库系统如何处理冷数据,解释了 Hyper 使用虚拟页管理冷数据,Stoica 等人提出将热冷数据分离到不同的内存位置。最后,论文描述了反缓存(anti-caching)的工作原理,并指出了其两个缺点:节省空间有限和重复执行。
  • 论文详细描述了将 SIBERIA 集成到 HEKATON 后的工作原理。它展示了数据迁移到冷存储期间使用的两种事务的工作流程。插入和更新操作确保数据被放入热存储,以避免检查冷存储的开销。删除和读取操作利用更新备忘录中的通知执行并发控制和冲突检测。冷存储清理也由更新备忘录驱动,这使得可以及时从冷存储中移除对任何活跃事务都不可见的记录。此外,验证阶段也利用更新备忘录中的通知,并计算 TsBoundSer 以确保可串行化事务的正确性,从而增强了幻读检测。
  • 论文呈现了相对全面的实验。实验基于 YCSB 基准测试和多步骤读/更新工作负载,这可以测试 Siberia 在不同工作负载下的性能。此外,论文评估了 Siberia 的纯内存开销、运行实时迁移的开销以及访问冷记录路径上更新备忘录的开销。并且,在 YCSB 工作负载下,展示了工作负载偏斜度、内存与数据库大小比率与工作负载性能之间的关系。

论文不足

  • 论文未在代码层面描述 Siberia 集成到 HEKATON 的过程,仅提到了在数据处理和存储机制层面进行集成。它详细描述了冷数据迁移过程,并解释了插入、删除、读取和更新操作中的更新备忘录通知如何与 HEKATON 的版本控制和并发控制协作。然而,论文未涉及 Siberia 如何在代码层面(如识别核心函数、代码段及相应修改)集成到 HEKATON。这一缺失使得读者难以轻松复现 Siberia。因此,论文至少应提供代码修改的概要说明。
  • 在 Synthetic End-to-End Workload 部分,论文未讨论中等至高冷数据访问率下的吞吐量损失。论文仅展示了在 5% 和 10% 冷数据访问率下的吞吐量损失,并声称这些是现实的冷数据访问率。首先,在 Read-Only Transactions 部分,论文未能说明是哪个报告或文献支持 5% 和 10% 是“真实的冷数据访问率”。其次,即使假设上述数据是准确的,论文也未描述当冷数据访问率超过 10% 时,吞吐量损失如何变化。这一缺失使读者无法全面了解 Siberia 处理工作负载的性能。因此,论文应解释“现实的冷数据访问率”是如何确定的,并描述在中等至高冷数据访问率下吞吐量损失的变化情况。
  • 论文未讨论 Siberia 的性能限制或其性能可能下降的条件,尽管实验部分展示了令人印象深刻的性能。这种优异的性能可能是以高硬件利用率(如高 CPU 使用率)为代价获得的。或者,将 Siberia 集成到 HEKATON 中虽然增强了热冷数据处理能力,但可能损害了 HEKATON 原有的某些特性。因此,论文应阐明 Siberia 性能可能下降的情形。

参考文献: https://www.vldb.org/pvldb/vol7/p931-eldawy.pdf

摘要

本文解决了一个核心问题:

  1. 如何设计一种高效的并发控制算法,以提升 OLTP 数据库管理系统在多核环境下的可扩展性?

本文提出了 4 个新颖的想法,并详细阐述了每个想法的实现方案:

  1. TicToc 算法: 在乐观并发控制 (OCC) 基础上实现。其核心在于事务的时间戳将延迟到提交时刻,根据该事务处理的所有元组进行计算。这种方式也提高了并行性。
  2. 验证阶段的无等待锁定 (No-wait locking in validation phase): 如果事务无法为其写集 (write set) 中的某个元组获取锁,验证阶段将立即中止。TicToc 会在等待一段时间后重启该阶段。
  3. 抢占式中止 (Preemptive aborts): 基于一个近似的提交时间戳以及元组的本地读时间戳 (rts) 和写时间戳 (wts),可以在锁定写集中的元组之前就判断是否应中止该事务。
  4. 时间戳历史记录 (Timestamp history): 当读取的元组的本地读时间戳 (rts) 低于事务的 commit_ts,且其写时间戳 (wts) 与最新 wts 不同时,会进一步检查该元组的历史缓冲区 (history buffer),以决定是否启动验证阶段。

分析与实验结果: 本文比较了五种算法:TicToc, Silo, HEKATON, DL_DETECT 和 NO_WAIT。

  • TPC-C 结果: 在 4 仓规模下,TicToc 实现了最高的吞吐量,且中止率低于 Silo。随着仓数增加,TicToc 的吞吐量最终在约 80 仓时被 Silo 超越,但其中止率仍低于 Silo。
  • YCSB 结果:
    • 处理只读事务时,TicToc 的吞吐量与 Silo 接近,并高于其他算法。
    • 在中等竞争条件下处理读写事务时,TicToc 的吞吐量与 Silo 相当,但其中止率显著低于 Silo、HEKATON 和 NO_WAIT。
    • 在高竞争条件下,TicToc 的吞吐量远超 Silo,尽管其中止率变得与 Silo 更为接近。
  • 优化测试: 表明大部分性能提升来源于无等待锁定和抢占式中止。此外,TicToc 的时间戳增长速度远低于 TS_ALLOC。
  • 隔离级别影响: 当隔离级别降低时,TicToc 显示出吞吐量的提升和中止率的下降,但这些变化的程度与其他算法相比并不显著。

论文优势

  • 背景与问题阐述清晰: 论文清晰地描述了背景知识和待解决的问题。它详细阐述了二阶段锁 (2PL) 策略的弱点,并指出基于时间戳排序 (T/O) 的策略(如 MVCC 和 OCC)已逐渐成为主流。接着指出传统 T/O 算法中集中式时间戳分配器 (centralized timestamp allocator) 和 CPU 的缓存一致性协议 (cache coherence protocol) 导致了时间戳分配瓶颈。此外,文中提到的所有硬件解决方案均未能完美契合大多数现代 CPU 的架构,即使实现其性能也欠佳。论文还简要描述了 OCC 的执行阶段,并介绍了两种基于 OCC 的优化方法(即 DTA 和 Silo),同时指出这两种方案仍存在可扩展性瓶颈。为解决这些问题,论文提出了 TicToc 算法。
  • 核心流程描述详实: 论文为 TicToc 算法的核心流程提供了代码、图表和示例,使读者能快速理解实现细节和工作流程。
    • 在 3.2 节,论文展示了读取阶段 (Read Phase)、验证阶段 (Validation Phase) 和写入阶段 (Write Phase) 的伪代码,清晰地说明了其设计如何应对并发和并行场景下的冲突,以及分布式时间戳分配。
    • 在 3.6 节,论文展示了用于存储读/写时间戳的结构,并通过伪代码有效地呈现了解决 delta 属性潜在溢出问题的方案。
    • 在 3.3 节,论文提供了一个交错执行事务的示例,并辅以条形图,清晰地展示了 TicToc 在处理并发和并行挑战时的高灵活性和性能。
  • 实验评估全面: 论文在 DBx1000 上对 TicToc 进行了全面的实验评估。
    • 在 TPC-C 和 YCSB 场景下评估了 TicToc 的性能。
    • 比较了在不同竞争级别和不同仓数下,TicToc 与 Silo、HEKATON、DL_DETECT、NO_WAIT 的吞吐量和中止率。
    • 评估了 TicToc 各项优化的效果,强调了无等待锁定和抢占式中止对性能提升的贡献。
    • 比较了 TicToc 的时间戳增长速率与线性时间戳增长速率。
    • 比较了在不同隔离级别下,TicToc 与其他 4 种算法在吞吐量和中止率上的差异。

论文不足

  • 集成过程缺失: 论文未展示将 TicToc 集成到 DBx1000 的过程。作为一个并发控制算法,TicToc 必须与其他关键组件(如事务、索引、日志)交互,这涉及大量工作。然而,论文未涉及此方面,使读者难以复现该算法。因此,论文至少应简要概述必要的集成步骤,以帮助读者实现此功能。
  • 普适性验证不足: 论文的实验未能证明 TicToc 在多种数据库上的通用性。评估仅在 DBx1000 环境中进行,因此仅证实了 TicToc 在 DBx1000 内的高性能。但许多商用数据库(如 SQL Server、MySQL 等)在不同工作负载下表现出不同的特性,这可能导致使用 TicToc 时性能表现各异。然而,论文对此方面完全未提及。因此,论文还应包含 TicToc 在这些主流数据库上的集成和测试工作。
  • 关键优化细节缺失: 论文在关键的 OPTIMIZATIONS 章节中,未能为某些关键概念提供代码或详细解释。根据实验结果,无等待锁定和抢占式中止带来了显著的性能提升。然而,优化部分并未呈现任何相关代码。
    • 例如,在 No-Wait Locking in Validation Phase 小节,论文既未阐明给定上下文中抖动问题 (thrashing problems) 的具体含义,也未展示无等待锁定的代码,或指出其相对于原始 TicToc 实现的修改之处。
    • 因此,论文应包含优化代码以及对相关概念的解释。

参考文献: https://people.csail.mit.edu/sanchez/papers/2016.tictoc.sigmod.pdf

摘要

该论文解决了三个问题:

  1. 如何减少复杂但快速查询的编译时间?
  2. 如何减少极大查询的编译时间?
  3. 如何减少首次传入查询的编译时间?

论文提出了两个新颖的想法,并详细阐述了每个想法的实现方法:

  1. 自适应执行 (Adaptive execution): 对于特定的查询流水线(pipeline),该方案跟踪工作线程的进度,基于流水线中所有线程的整体状态预测三种执行模式下剩余工作负载的持续时间,最终选择持续时间最短的模式应用于所有线程。
  2. 快速字节码解释 (Fast bytecode interpretation): 基于寄存器机器,该方案通过按区间(intervals)处理基本块(basic block)并利用并查集(disjoint set)和路径压缩(path compression)等算法,实现了线性时间的活性计算(liveness computation),从而进一步优化了寄存器分配。此外,字节码解释器的行为与生成的机器码等效,确保了在解释执行和机器码执行之间可以无缝切换。

分析与实验结果:对于不同的比例因子(scale factors),自适应执行能够切换到具有最优性能的模式,确保相比其他静态模式选择具有最低的执行时间。在行动方面,自适应执行能够立即在所有可用工作线程上开始处理流水线分片(pipeline morsels),并对工作负载繁重的流水线进行动态模式切换,使其完成查询的速度比竞争对手快10%、40%和80%。虽然解释执行的代码比编译后的代码慢,但它比PostgreSQL快,并且在使用多核时能像编译代码一样扩展。此外,字节码解释器具有完美的扩展性,能够在短时间内处理大型查询。

论文优势

  • 论文清晰详尽地描述了待解决的问题。在第二节中,通过流程图展示了SQL查询在HyPer中的多步骤处理过程,并强调LLVM编译任务占据了总执行时间的大部分。通过比较在比例因子1的TPC-H Query 1上不同执行模式的编译和执行时间,论文引入了解释器和编译器之间的权衡,论证了可以对同一查询的不同部分应用不同的执行模式。此外,论文分析了最大的TPC-H和TPC-DS查询,得出结论:编译时间随查询规模呈超线性增长。
  • 论文提供了全面的文献综述。在第六节中,引用了其他关于编译时间的研究论文中的实验结果,得出结论:编译到LLVM中间表示(IR)比编译到C语言更快。基于实践经验,论文强调查询编译延迟在生产环境中成为一个主要问题,这使得自适应执行成为使查询编译真正实用化的关键组件。随后探讨了将自适应执行集成到MemSQL、LegoBase和Microsoft Hekaton等数据库系统中的可行性。此外,论文展示了自适应执行相对于自动计划缓存(automatic plan caching)的优势,即能够在每次执行时重新优化查询。最后,讨论了自适应执行与编程语言中执行引擎(如JVM、V8和CLR)的异同。
  • 论文取得了显著的改进,即线性时间的活性计算。传统逐个基本块计算活性的解决方案通常需要$O(n^2)$的运行时间。然而,论文提出了一种线性时间算法。该算法以逆后序(reverse postorder)标记所有基本块并将其组织成支配树(dominator tree),这使得解释器能够在$O(1)$时间内确定基本块之间的关系,并为识别循环铺平了道路。它通过使用带路径压缩的并查集(disjoint set with path compression)来识别每个基本块的最内层封闭循环(innermost enclosing loop),并基于与某个值的定义和使用相关的基本块分布来确定该值的生命周期。该计算的低成本主要归功于采用了适当的数据结构。

论文不足

  • 论文未清晰解释基本概念。在第二节中,没有解释延迟和吞吐量在HyPer上下文中的具体含义,以及两者之间的关系,因此读者可能无法认识到权衡的重要性,也可能不理解后续实验结果中性能提升是如何实现的。例如,读者可能无法理解为什么解释器能通过牺牲吞吐量来实现非常低的延迟。因此,论文应解释这些概念。
  • 论文的实验不够全面。作为数据处理和存储的核心组件,自适应执行框架仅被证明在执行时间上具有优势,而缺乏展示其物理设备利用率、稳定性和故障恢复性能的实验。这些指标对于评估框架的整体性能同样至关重要。例如,对于同一组查询,如果执行时间短但CPU和内存使用率极高,或者抛出异常的概率高且需要大量时间进行故障恢复,那么该框架仍有改进空间。因此,论文应包含这些指标的实验结果。
  • 论文未详细解释如何翻译成虚拟机代码。在第四节B小节中,论文提到被归约(subsumed)的指令不会被翻译,但未具体说明哪些类型的指令会被归约,或者以何种方式被归约。这些遗漏会使读者感到困惑,阻碍他们对翻译伪代码的理解。因此,论文应解释归约指令背后的原理。

参考文献: https://db.in.tum.de/~leis/papers/adaptiveexecution.pdf

摘要

本文解决了两个问题:

  1. 如何设计一个能够处理大数据和复杂分析查询,同时确保生成高效查询计划的查询优化器。
  2. 探索高级查询优化理论在生产环境中的应用。

本文提出了4个新颖的想法,并详细阐述了每个想法的实现方法:

  1. 通过DXL将优化器与数据库系统解耦:不同的数据库系统需要实现3个转换器(Query2DXL、MD Provider 和 DXL2Plan)来支持Orca。
  2. 使用Memo和组哈希表进行优化:Memo中的每个组存储给定操作的所有逻辑等价表达式(包括强制执行运算符)。组哈希表记录每个优化请求的最优实现(即最佳组表达式)。在查询优化期间,通过检索每个组及其子组对应于给定优化请求的最佳组表达式(best GExprs),将这些最佳GExprs链接在一起形成最佳执行计划。
  3. 利用作业依赖图和作业队列实现并行查询优化:如果一个组当前正在处理一个优化作业,其他作业将被放入队列等待。一旦作业完成,其结果可供后续作业利用。
  4. 开发高效的测试工具:AMPERe用于捕获错误并生成转储文件以供后续重放调试。TAQO基于优化请求的链接结构均匀采样计划,并通过计算基于估计成本的采样计划排序与基于实际成本的排序之间的相关性得分,来评估优化器的准确性。

分析与实验结果:基于TPC-DS基准测试,使用有限的查询集测试了GPDB传统查询优化器(111个查询,MPP)和Orca,以及Impala(31个查询,Hadoop)、Stinger(19个查询,Hadoop)、Presto(12个查询,Hadoop)和HAWQ。结果表明,在80%的查询执行中,Orca的性能匹配或优于GPDB优化器。对于14个查询,Orca相比GPDB优化器实现了至少1000倍的加速比。Presto在所有测试条件下均未能处理任何TPC-DS查询,而HAWQ上的查询执行性能普遍优于Impala和Stinger。

论文优势

  • 论文提供了全面的文献综述,并涵盖了理解Orca所需的预备知识。在 PRELIMINARIES 部分,论文简要分析了GPDB,解释了其3个核心组件:主节点(master)、段节点(segments)和互联层(interconnect layer);阐述了SQL和查询优化器如何集成到Hadoop等大数据处理组件中;并分析了由Orca优化的HAWQ架构相比Impala和Presto的优势。在 RELATED WORK 部分,论文介绍了Volcano和Cascades框架,强调Cascades比Volcano提供了更大的灵活性;随后讨论了MPP数据库中各种面向大数据的查询优化器实现,如PDW和SCOPE;最后回顾了将SQL与Hadoop集成的现有努力,例如将查询转换为MapReduce作业(Hive)以及数据库管理系统与Hadoop技术的共置(Hadapt)。
  • 论文提出了针对大数据查询优化的极具价值的解决方案:DXL和并行查询优化。不同的数据库只需实现各自的Query2DXL和DXL2Plan转换器即可实现与Orca的兼容,这赋予了Orca适配任何现有数据库系统的潜力。为满足大数据的核心需求——并发处理,Orca构建了优化作业依赖图来确定作业间的依赖关系。父作业必须在其子作业完成后才能执行,而独立的作业可以并行运行。为处理资源争用,Orca将传入的作业放入作业队列中等待,直到正在运行的作业完成。这些等待的作业随后可以利用已完成作业生成的结果。
  • 论文在 QUERY OPTIMIZATION 部分详细阐述了Orca优化的步骤。在探索(Exploration)阶段,Orca创建逻辑等价表达式并使用Memo进行去重。在统计推导(Statistics Derivation)阶段,Orca估算Memo组的基数和数据倾斜。在实现(Implementation)阶段,Orca生成逻辑表达式的物理实现。在优化(Optimization)阶段,生成多个执行计划(必要时包含强制执行运算符),然后使用成本模型选择成本最低的执行计划。这些细节有效地帮助读者对Orca的工作原理建立起高层次的理解。

论文不足

  • 论文缺乏对优化阶段如何计算执行计划成本的描述。具体而言,完全未讨论成本模型,而这应是Orca的核心功能。这一点在处理大数据和无共享架构(shared-nothing architecture)时尤为重要,此处的成本模型可能不同于Selinger成本模型,需要纳入跨多个工作节点的协调和通信成本,并考虑网络带宽。建议论文包含对成本模型的详细描述,并讨论其在单体式和分布式数据库系统中的行为。
  • 论文针对MPP数据库的实验评估基于有限的数据集。Orca仅与GPDB传统查询优化器使用119个查询进行了比较。无论是查询数量还是被比较的MPP数据库优化器的数量都不足以令人信服地证明Orca相对于其他MPP数据库优化器的优势。因此,实验应进行更广泛的MPP数据库比较,例如与Amazon Redshift和Teradata Aster进行比较,以提供更全面的评估。
  • 论文的实验未反映Orca的硬件利用率,如CPU使用率、内存消耗等。Orca很可能会在无共享架构中运行,它将跨多个服务器运行。然而,在生产环境中,这些服务器不太可能专用于Orca,包括Orca将要优化的数据库在内的其他进程很可能与Orca运行在同一台服务器上。如果Orca的CPU或内存使用率过高,可能会对这些数据库的查询执行以及其他应用程序的性能产生负面影响。这种影响可能在多台服务器上累积,导致显著的性能下降。因此,论文还应包含对Orca硬件资源消耗的评估。

参考文献: https://dl.acm.org/doi/10.1145/2588555.2595637

查询执行

在数据库查询执行中,查询管道(pipelining)和物化(materialization)是两种核心的执行模型。查询管道通过让元组在操作符之间持续流动,实现了流水线式的处理,从而减少了中间结果的写入和读取开销;而物化模型则在每个操作之后将中间结果保存为临时表,再由下一个操作读取,便于管理和重用,但会引入额外的 I/O 和存储成本。

在数据库查询执行中,要在既定的查询计划不变的前提下进一步提升性能,常见的三大技术杠杆正好对应经典的 CPU 性能方程

CPU Time=Instruction Count×Cycles Per Instruction×Clock Cycle Time \text{CPU Time} = \text{Instruction Count} \times \text{Cycles Per Instruction} \times \text{Clock Cycle Time}

基于这一方程,我们可以通过以下三种途径分别作用于各项,从而整体降低查询的执行时间。

  1. 减少指令数:通过让同样的计算工作消耗更少的指令,直接降低方程中的 Instruction Count

    解决方案:

    • 算法与逻辑优化:在查询执行层面,合并相邻的过滤或算子,避免产生冗余的中间操作,如将多重过滤合并成单次扫描中的复合条件。
    • 编译器与JIT优化:利用现代数据库的即时编译(JIT)技术,将高频 SQL 模板编译成高效机器码,去除不必要的抽象层次。
    • 简化控制流:减少分支与函数调用,利用内联(inlining)与分支预测友好的代码组织,降低分支错判带来的指令重执行。
  2. 降低每条指令的平均周期数:在保证指令数大致不变的情况下,通过加速每条指令的执行来降低平均周期数,使得每个周期内能执行更多的指令。

    解决方案:

    • 向量化执行:一次处理多个数据元素(SIMD),使单条指令能够并行作用于多份数据,显著减少每元素的平均周期开销。
    • 缓存友好布局:优化内存访问模式、减少缓存未命中、利用预取(prefetch)技术,降低内存访问延迟对 CPI 的影响。
    • 流水线与乱序执行:让 CPU 同时处理多条指令的不同阶段,掩盖指令间的依赖与等待,提高指令吞吐能力。
  3. 并行化执行:利用多线程/多进程将查询拆分为若干可并行处理的子任务,以共享整个系统的计算资源。

    解决方案:

    • 并行扫描 (Parallel Scan):将表或分区划分为多个数据块,每个 worker 进程或线程独立扫描其负责的数据块。
    • 并行聚合 (Parallel Aggregation):类似 MapReduce,在每个节点本地先做部分聚合,再在汇总阶段合并结果,降低网络与锁竞争开销。
    • 动态分区与调度:根据实时负载与数据分布,自动调整任务划分与调度策略,避免因数据倾斜或节点瓶颈造成性能倒挂。

在现代关系型数据库中,**查询执行(Query Execution)**依赖于三个核心概念:查询计划、操作符实例和任务/流水线。查询计划被表示成一个操作符的有向无环图(DAG),它定义了不同运算节点及数据流依赖;操作符实例则是将某一操作符应用到数据的具体调用;任务(也称流水线)由一个或多个操作符实例顺序组成,在同一执行上下文中连续运行,从而实现元组的流式处理和高效并行执行。

假设我们要执行如下 SQL 命令:

1
2
3
4
SELECT A.id, B.val
FROM A JOIN B
ON A.id = B.id
WHERE A.val < 88 AND B.val > 214;

此时的查询执行的示意图如下:

img

但是在以上的查询执行中,会出现两种普遍发生的性能瓶颈:

  1. 指令依赖:当一条指令的操作数依赖于前一条指令的输出时,就会出现数据依赖,导致后续指令必须等待前一指令完成写回,才能安全地执行。这会导致流水线停顿(Pipeline Stall),也就是依赖链未解除之前,流水线必须插入空周期,暂停取指与执行,直到数据可用。每出现一次停顿,均减少了可用的执行周期,尤其在长依赖链或深度流水线中,性能损失更为显著。

    解决方案:

    1. 前递(Forwarding/Bypassing):处理器在执行阶段即可将尚未写回的结果转发给下游指令,减少等待时间。
    2. 指令重新排序(Out-of-Order Execution):乱序发射允许非依赖指令先行执行,填补停顿周期,提高资源利用率。
    3. 软件优化:编译器或 JIT 在生成代码时,通过指令调度将依赖指令错开,或插入无关指令填充空档,降低数据冒险带来的停顿。
  2. 分支预测:处理器在遇到分支指令时,会猜测分支是否成立,并预取/预执行分支路径上的指令,以保持流水线连续。这会导致流水线冲刷(Pipeline Flush):若预测错误,必须将已加载但未提交的指令全部作废,并重新从正确目标地址逐级取指,造成数十至上百个周期的空闲;以及分支惩罚随流水线深度增加而增大:深度更高的流水线需要冲刷更多级的指令,带来更高的性能损失。

    缓解方案:

    1. 动态分支预测:利用分支历史表(Branch History Table)等硬件结构根据过去执行情况进行预测,大幅提高准确率。
    2. 无分支代码:使用位运算和逻辑运算消除显式的 if 判断语句,从而让 CPU 不依赖分支预测。
    3. 索引下推:将分支条件判断尽量前移到索引扫描阶段,减少进入后续流水线的无效数据。
    4. 向量化执行:批量处理多条记录,将过滤逻辑应用于整个向量数组,在单条循环中执行多次条件判断并记录掩码,减少分支次数。

    [!NOTE]

    数据库中的分支失误:过滤操作

    在顺序扫描过程中,每访问一行记录,都要执行对该行是否满足过滤条件的判断(if 语句),这构成了 DBMS 中最频繁的分支指令序列。由于行数据的分布通常不可预测,过滤条件的真/假结果几乎是随机的,硬件分支预测器难以积累稳定的历史。

    对于这种情况,我们可以采用无分支代码来解决。假设我们要执行如下命令:

    1
    SELECT * FROM user WHERE age > $(low) AND age < $(high);

    那么这个 SQL 命令对应的有分支的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    i = 0;
    for (t in table) {
    key = t.key;
    if ((key > low) && (key < high)) {
    copy(t, output[i]);
    i = i + 1;
    }
    }

    在以上代码中,每次 if 检查都生成一条或多条分支,过滤条件结果依赖于数据值分布,通常呈随机模式,硬件分支预测器难以积累有效历史,同时预测错误时需刷新流水线,放弃已取但未执行完的指令,造成 10–20 个周期的空洞。

    因此我们可以将其改成无分支的代码:

    1
    2
    3
    4
    5
    6
    7
    i = 0;
    for (t in table) {
    copy(t, output[i]);
    key = t.key;
    delta = (key > low ? 1 : 0) & (key < high ? 1 : 0);
    i = i + delta;
    }

    这段代码使用三目运算符和按位与(&)生成布尔掩码(0 或 1),避免 if 分支,消除了流水线刷新风险,可保持稳定的指令流,而且无分支的循环更符合 SIMD 自动向量化特性,编译器和 JIT 能将单条循环转换为批量处理指令,并利用 AVX/SSE 等向量指令集。

处理模型

在关系型数据库中,**处理模型(Processing Model)**决定了执行器如何遍历并执行优化器生成的查询计划树。不同处理模型在函数调用开销、中间结果管理、内存与 I/O 使用等方面各有取舍,因而对 OLTP(短事务、小结果集)与 OLAP(大批量分析查询)场景会产生截然不同的性能影响。

迭代器模型(Iterator / Volcano Model)

每个查询计划算子实现一个 next() 方法:调用时返回一个元组或标记结束(null),而且 next() 内部通常是一个循环:对子算子连续调用 next() 以获取下一个元组并进行处理,实现拉取式(pull-based)流水线

在该模型中,JOINORDER BY 和子查询等算子却会一直被阻塞直到子算子发送完了所有的元祖。

下图展示了迭代器模型的执行流程,其中绿色箭头大小控制流,红色箭头代表数据流,之后的图都是如此:

img

上图中,在系统层面,这两个流水线各自由不同的执行线程或调度实体运行,可以在硬件资源(CPU 核、线程)许可下同时活跃。但哈希连接的建表阶段是一个管道断裂点(pipeline breaker):建表阶段只依赖 R 表,与 Pipeline #2 无关,因此当建表线程在后台扫描 R 时,Pipeline #2 可以同时并发进行 S 表的过滤。一旦建表完成,Pipeline #2 的探测阶段才会真正开始消费 Filter 的输出;如果 Filter 还没产出下一个符合条件的行,探测就会等待。反之,如果过滤很快,探测也会快速拉取并处理。

[!NOTE]

拉取式流水线中,调用者(上层算子或执行引擎)通过自顶向下的递归调用不断拉取数据,直到叶子算子从底层存储读取到元组并逐层返回。这种设计将控制流和数据流分离,上层算子负责驱动执行进程,下层算子被动提供数据,适合行级的管道处理。

优点:

  1. 内存占用小:元组生成后可立即沿计划树上下游传递,无需完整物化中间结果。
  2. 早期返回:可以在部分数据处理完后就立即返回这部分的结果。

缺点:

  1. 函数调用开销:深度计划树和高频 next() 调用在高并行度或大结果集场景下带来显著开销。
  2. 难以并行化:对多核或批处理的支持不够友好。

因此,该模型在 OLTP 场景中表现优异。

物化模型(Materialization Model)

Materialization 模型是数据库管理系统中常见的查询执行模型之一,其中每个操作符一次性处理所有输入并一次性输出所有结果,称为物化操作 。该模型适用于 OLTP 场景,因其函数调用较少、协调开销低,能够快速返回少量元组;但对于中间结果集庞大的 OLAP 查询,则可能因临时结果需要写入磁盘而导致性能下降 。DBMS 可通过下推诸如 LIMIT 的执行提示(hints)来避免扫描过多元组,并支持行式存储或列式存储的物化输出方式。

img

向量化模型(Vectorization Model)

Vectorization 模型是一种批量处理的查询执行策略,其中每个操作符在内部循环中一次性读取并处理一批(batch)的元组,然后一次性向外输出该批结果。这种方式通过利用现代 CPU 的 SIMD 指令加速批量数据处理,显著降低函数调用次数并提升缓存命中率,因而特别适合 OLAP 查询;但对于需要低延迟、逐一元组处理的 OLTP 工作负载,则并不理想。该模型的批大小可根据硬件能力(如 AVX 寄存器宽度)和查询属性动态调整,以实现最优性能。

每个查询操作符实现一个 next() 函数,但不是返回单个元组,而是返回一批元组。操作符的内部循环一次性在向量或批量数据上执行相同的操作,而非逐个元组调用。

img

需要强调的是,以上的三种处理模型都是属于 pull-based 的。

Pull 模型从根节点发起,通过连续调用 next() 向下拉取数据,适合传统行式、阻塞操作较多的场景;Push 模型则从叶子节点开始,将处理结果推送给父节点,并可在单一循环内融合多个算子,减少中间结果存储,更适合 OLAP 工作负载。

Push-based 模型的示意图如下:

img

Pull-based 和 Push-based 模型的对比

特性/模型 Pull(Top-to-Bottom) Push(Bottom-to-Top)
输出控制 易于对 LIMIT、TOP-N 等操作进行控制,父算子可随时停止拉取 不易实现 LIMIT,消费者难以及时通知生产者停止推送
管道阻塞支持 原生支持排序、聚合等阻塞算子 复杂算子需额外物化或流水线断点才能正确执行
函数调用开销 每条元组多次调用 next(),虚拟函数和分支跳转较多 同一循环内推送,函数和分支开销更低
缓存 & SIMD 利用 难以批量处理,SIMD 和缓存局部性利用有限 算子融合后易于批量处理,充分利用缓存和 SIMD
实现复杂度 基于 Volcano 模型,逻辑清晰、易于实现与维护 需算子融合、回调机制和调度框架,开发调试成本高
适用场景 OLTP、交互式小批量查询,带 LIMIT 的场景 OLAP、大规模扫描与聚合,列式存储与 MPP 系统