凌晨的分析任务要扫描数十亿条订单,再把订单与商品、用户、活动数据连接起来,最后按地区与品类汇总。单台服务器并不是完全做不了,但它必须依次读取数据、执行连接、写出中间结果,完成时间可能从分钟拖到小时。并行数据库换了一种执行方式:数据分散存放,多组处理单元同时读取、计算和交换结果,最后把局部结果合成一个逻辑答案。
难点也正是在“同时”二字。多加节点不会自动得到等比例加速。启动任务、跨节点传输、共享资源争用、热点键和慢节点都会吞掉并行收益。我们真正要设计的是一条完整协作链:数据怎样分,算子怎样并行,什么时候交换数据,分给多少资源,某个任务落后或失败时怎样继续。

数据库性能最常见的两个观察角度是吞吐量和响应时间。吞吐量表示单位时间完成多少任务,响应时间表示一个任务从提交到完成经历多久。大量短事务通常更关心吞吐量:让不同事务在不同核心或节点上同时执行,就能在一秒内处理更多请求。长时间运行的分析查询更关心响应时间:必须把同一条查询拆成可并发的子任务,单条查询才会更快。
这一区分会直接影响设计。假设一个订单查询原本要 40 秒。如果系统只是同时执行 20 条这样的查询,整体吞吐量提高了,单条查询仍然可能是 40 秒;只有把扫描、连接和聚合拆到多个执行单元,同一条查询的延迟才可能下降。反过来,给每条极短点查询都启动数十个 worker,协调开销反而可能比查询本身更大。
关系查询天然以集合为对象。对十亿行做筛选时,可以把行分成许多片,每个执行单元处理自己的片;等值连接可以让连接键相同的元组流向同一个执行单元;分组聚合可以先算局部和,再合并成全局和。只要局部计算满足正确的分解规则,多个局部答案就能组成完整答案。
并行数据库通常把资源组织成节点。一个节点有处理器、内存和持久化存储,节点之间由高速网络连接。现代系统往往是层次化的:单个节点内部使用多核与共享内存,节点之间按共享无方式组织。于是同一条查询同时包含两种距离不同的通信:节点内线程共享内存,节点间任务发送网络数据。后者通常更昂贵,执行计划需要尽量让计算靠近数据。
并行数据库向使用者保留统一的数据模型与查询接口。应用提交的仍是一条 SQL;数据分区、任务调度和结果归并主要由数据库完成。并行性是执行层的能力,不应迫使业务代码手工拼接每个分片的答案。
设同一任务在基准系统上的运行时间是 ,使用 份处理资源后的时间是 。加速比定义为:
理想情况下,资源增加到 倍,执行时间降为原来的 ,于是 ,这叫线性加速。现实曲线通常低于这条理想直线。偶尔会出现超线性加速,例如扩容后工作集全部进入内存,系统省掉了原先大量磁盘 I/O;这种现象来自存储层级发生变化,不能当作可以无限复制的规律。
扩展比观察的是另一件事:资源扩大 倍,同时让工作量也扩大 倍,完成时间能否大致保持不变。批处理扩展把数据库规模或扫描量当作问题规模;事务扩展同时增加数据库规模和事务到达率。对持续增长的业务,扩展比往往比一次跑分的加速比更有意义,因为系统真正需要的是数据与请求增长后仍维持可接受的服务水平。
若任务中可并行部分占比为 ,其余 必须串行执行,使用 个执行单元时,理想化运行时间比例为:
因此加速比为:
即使把 增大到极大,串行部分仍然存在。若只有 90% 的工作可并行,理论加速上限就是 10 倍。并行度继续升高后,启动任务、网络交换和结果汇总还会继续增加,实际曲线会更早变平。
除了串行部分,常见损耗还有三类。第一是启动开销:创建线程、分发计划和建立网络通道都要时间。第二是干扰:并发任务争用内存带宽、缓存、磁盘、网络和锁。第三是倾斜:整体完成时间由最慢分片决定,其他执行单元即使早已空闲,也不能让查询提前结束。

并行查询算法经常用共享无架构来描述,因为节点之间的数据移动最显式。但真正运行环境通常会混合多种资源组织方式。
共享内存系统里的同一个地址,并不意味着每个核心访问它都一样快。现代多路服务器常使用 NUMA:某段内存与某个处理器更接近,本地访问更快,跨处理器访问更慢。线程最好保持在相同核心或相同 NUMA 节点,内存也尽量在首次使用它的处理器附近分配。并行哈希连接若忽略这一点,可能把大量时间花在远端内存访问和缓存一致性流量上。
节点间交换不是抽象的“传一下”。总线一次只能服务有限通信,节点增加后会争用;环形网络可让多条链路同时工作,但远端消息要经过多跳;网格的总链路数随节点增加而增加;数据中心常使用树形或胖树形网络,由机架内交换机、汇聚层与核心层连接。机架内带宽往往比跨机架更充足,因此副本位置和数据交换路径都会影响可用带宽。
以太网、InfiniBand 和 RDMA 等技术可以降低延迟或提高吞吐。RDMA 允许一个节点直接读写另一个节点授权的内存区域,减少操作系统协议栈和数据复制。但更快的网络不会取消数据移动成本。若执行计划把 2 TB 中间结果重分区,压缩、序列化、队列等待和接收端反序列化仍然存在。

最底层的 I/O 并行可以把连续块轮流放到多块磁盘,使大范围读取同时使用多条设备通道。数据库还需要更高一层的控制:哪些元组在哪个节点。元组级水平分区既决定扫描能否并行,也决定点查、范围查与连接需要访问哪些节点。一个节点内部如何把块放到多块设备,可继续交给本地存储或 RAID;查询执行层更关心节点之间的分布。
轮询分区按到达顺序把第 个元组放到第 个分区。它容易得到接近相等的元组数,适合全表扫描,但没有从属性值到节点的直接映射。点查或范围查通常要访问所有分区。
哈希分区选择一个或多个分区属性,对属性值计算哈希并映射到节点。若查询给出分区键的等值条件,就能直接路由到一个分区;全表扫描也能并发进行。哈希打乱了值的邻近关系,所以分区键范围查询通常仍需扫描多个节点。
范围分区用有序边界向量切分属性域。例如边界 [1000, 5000, 20000] 把客户编号划成四个连续范围。点查可通过边界定位节点,范围查只访问与查询范围相交的分区。问题是业务值通常并不均匀:边界等宽不等于数据等量,近期时间段或热门地区很容易形成热点。
更新分区键时要把旧元组从原分区删除,再把新元组插入目标分区。这个动作不是单页原地修改,而是跨分区迁移。小表也不一定值得切到所有节点:当每个分区只有少量记录时,任务启动和元数据管理成本可能超过并行收益;此时保留单份,或将小表复制到需要它的节点,往往更合适。

属性值倾斜表示少数值重复得异常多,例如大量订单都属于“未知地区”;无论范围还是哈希,这些相同值通常都会进入同一个分区。分区倾斜表示边界或哈希映射本身造成数据量不均,即使属性值没有明显热点,也可能出现某些分区更大。执行倾斜则不要求数据量不均:四个分区大小相同,但查询几乎只访问“今天”的分区,今天所在节点仍会成为热点。
倾斜的代价由尾部任务决定。1000 个单位的工作均分到 10 个执行单元,理想每个承担 100;如果最慢者承担 200,整体加速最多约为 5 倍,而不是 10 倍。并行度提高后,平均分片变小,遇到一个明显大分片的相对损失更严重。
范围边界不能简单等宽切分。更稳妥的办法是根据频数表、直方图或随机采样估计分布,再选择近似等量的分位点。统计信息会过期,系统应同时监控分片大小、请求率、排队时间和实际算子进度,而不是只看建表时的分布。
虚拟分片把逻辑分片数设得远大于物理节点数。系统先把键映射到 tablet 或虚拟节点,再把许多虚拟节点分配给真实节点。若某台真实节点过载,可以迁移其中几个虚拟节点;扩容时也只需把部分虚拟节点搬到新节点,不必重写整张表。
如果某个虚拟分片自身持续长大,就在键空间中把它一分为二。一个负责旧范围,一个负责新范围,其中一个再迁到负载较低的节点。分区表记录“键范围 → 虚拟分片 → 真实节点”的映射,并通过路由器或客户端缓存加速定位。映射变化时必须有版本或刷新机制,避免请求继续发往旧位置。
存储分布平衡,不代表连接输出平衡。查询执行器可以把工作拆成许多比线程数更细的 morsel。执行单元每次领取一个,完成后再领下一个;空闲执行单元还可以从繁忙队列中窃取未开始的任务。在共享内存环境中,工作窃取通常无需搬运数据;在共享无集群里,要比较复制分片的网络代价与缩短尾部时间的收益。

查询间并行让多条独立查询同时运行,主要提高吞吐量。查询内并行把一条查询拆开,主要缩短长查询响应时间。实际系统会同时使用两者,但资源管理器必须避免一条大型分析把所有核心和内存占满,让短查询排队。
查询内并行又有两个层次。算子内并行把同一个扫描、排序、连接或聚合拆到多个执行单元上,每个单元处理不同数据分片。关系中的元组很多,因此这个层次可以提供很高并行度。算子间并行让执行树中的不同算子重叠运行,分为流水线并行和独立并行。
流水线中,上游算子产生一批元组后,下游立即消费,不必等完整中间结果写盘。并行环境通常偏好 push 模型:生产者主动把批次推入缓冲区,生产者和消费者可以同时运行。跨节点时,发送端和接收端都需要缓冲区;按批发送比逐元组发送能显著减少每条消息的固定开销。缓冲区满时要施加背压,否则快生产者会把内存耗尽。
独立并行处理互不依赖的子树。例如先同时计算 r1 ⋈ r2 与 r3 ⋈ r4,等两边完成后再连接两个中间结果。它受到执行树宽度限制,流水线也受到链长和阻塞算子限制,所以高并行度主要还是来自算子内的数据并行。
排序、完整聚合、构建某些哈希表等算子可能形成流水线边界:它们要看到全部或一个分区的全部输入后才能稳定地产生输出。并行计划因此不是一条从头到尾持续流动的管道,而是若干流水线阶段组成的有向无环图。阶段之间有依赖,阶段内部的算子可以重叠。

若关系已经按排序键做范围分区,只需每个节点本地排序,再按范围顺序拼接各节点输出。若原分区与排序键无关,可以先根据平衡的范围边界重分区:所有源节点一边扫描,一边把元组发往对应范围的目标节点;目标节点落盘或进入排序器,再独立完成本地排序。因为目标范围互不交叠,最终拼接不需要再次比较跨分区元组。
边界必须尽量按数据量平衡,而不是按数值区间等宽。常用办法是对各源分片采样,合并样本并求分位点。虚拟范围也能进一步摊薄误差:先建立比真实节点更多的范围,再把多个范围映射到一个节点。
另一种方法是各节点先对本地数据排序,然后用统一范围边界切开有序流。每个目标节点从多个源节点接收已经有序的子流,再做多路归并,得到自己的全局有序范围。这种做法把“排序后再交换”与“交换后再排序”的顺序对调了。
发送调度也会制造倾斜。若所有源节点先把第一个范围全部发给节点 1,再发第二个范围,接收端会轮流忙碌。更好的方式是把每个“源 → 目标”的有序流切成块,轮流发送各目标的下一块,让多个接收端持续并行。块太小会放大消息开销,块太大又会增加等待和内存占用。
从执行模型看,范围分区排序可以写成“范围 Exchange + 各节点本地排序”;并行外部归并可以写成“各节点本地排序 + 带有序归并的范围 Exchange”。这两个表达把并行机制集中到 Exchange,排序器本身仍可复用单机实现。

对 r.A = s.B,分区连接对 r.A 与 s.B 使用同一个哈希函数或同一组范围边界。两个输入的相同连接键必然到达同一目标节点,节点再执行本地哈希连接、归并连接或嵌套循环连接。若输入已按连接键同样分区,就能省掉一侧或两侧 Exchange;数据共置常常比单纯增加执行单元更有效。
本地采用哈希连接时,接收端可在元组到达时直接按本地哈希函数生成更小分区,省去一次先写完整接收分区、再读回重新分桶的过程。构建侧的一部分若能留在内存,还能使用混合哈希连接,让相应探测元组到达后直接查内存哈希表。
若一侧很小,可以把小表广播到所有处理大表分片的节点。每个节点本地执行“大表分片 ⋈ 完整小表”。网络传输量近似为小表大小乘以接收节点数;它可能远小于把大表按连接键重分区。广播是否划算不能只看行数,还要看压缩后字节数、目标并行度、可用内存和并发查询数量。
对不等值等难以共分区的条件,可使用更一般的分片复制:把两侧分别切成 和 片,安排 个局部组合,使每对可能匹配的分片都有机会相遇。它适用范围更广,但复制和比较成本也更高。外连接还要谨慎:一个分片中没匹配,不代表另一个分片也没匹配,不能过早输出带空值的行。
某个热门键在两表中分别出现 次和 次,单键就可能产生 条结果。即使输入行数均衡,连接工作量也会严重倾斜。优化器应结合直方图、高频值列表和不同值数量估计热点。运行时可把普通键按哈希处理,把热门键单独拆分、复制一侧,或把虚拟任务交给空闲节点执行。

筛选能否只访问少数节点,取决于谓词和分区键。对哈希分区键等值筛选,可直接路由到一个分区;对范围分区键的范围筛选,只访问相交分区;其他谓词通常要在所有节点本地执行。投影不去重时可以与扫描流水线执行,并尽早丢掉后续不需要的列,减少网络字节数。
去重可以把元组按去重键哈希或范围分区,再在每个节点本地去重。关键条件是相同键必须流向同一个目标节点,否则两个节点各自保留一份,合并后仍有重复。也可以借助并行排序,在归并时消除相邻重复项。
按键分组聚合的通用做法是:先按分组键重分区,再在目标节点计算。对 SUM、COUNT、MIN 和 MAX 等可合并聚合,可以先在每个源节点做局部聚合,把大量明细压成“分组键 + 局部状态”,Exchange 后再做全局合并。
例如平均值不能直接平均各节点的局部平均值,因为各节点行数不同。正确的局部状态是 (sum, count),全局再计算:
局部聚合同时缓解倾斜。某个热门键即使出现一亿次,每个源节点也可能先压成一条局部状态,再把少量状态发送给负责该键的目标节点。若聚合函数不能安全分解,或单组状态本身很大,就要用虚拟任务、键拆分或运行时重调度处理热点。
工程上最划算的优化常常发生在 Exchange 之前:早筛选、早投影、局部聚合和压缩编码都会减少网络传输。并行计划先减少必须移动的数据,再讨论要用多少节点。
Exchange 的价值在于隔离并行通信。它在源端决定每条数据发往哪里,在目标端合并来自多个源的输入;其他扫描、连接、排序和聚合算子只处理本地数据。常见路由有四种:按键哈希、按范围、广播到所有目标,以及把全部数据汇聚到单一目标。
目标端可以随机合并到达批次,也可以在各源输入有序时做有序多路归并。这样,范围排序可以表达为“范围 Exchange 后本地排序”,哈希连接可以表达为“两侧哈希 Exchange 后本地连接”,分组聚合可以表达为“按分组键 Exchange 后本地聚合”。已有单机算子不必了解整个集群拓扑。
设查询先连接订单与明细,再按地区聚合。若两表没有按连接键共置,计划先对两侧分别执行 Exchange;目标节点做本地哈希连接;连接结果可先局部聚合,再按地区执行下一次 Exchange;各目标节点完成全局聚合,最后根据接口需要,把分区结果保留或汇聚到协调节点。
Exchange 同时是管道与边界。发送端把本地 pull 算子产生的元组收集成批次,再以 push 方式发到目标;目标端把批次放入缓冲,让后续本地算子继续用 pull 接口消费。若下游变慢,Exchange 必须通过信用、队列上限或阻塞传播背压。

单机优化器要选择连接顺序、访问路径和物理算子。并行优化器还要决定每个中间结果的分区属性、在哪里插入 Exchange、是否广播、每个阶段使用多少执行单元、哪些阶段流水线、哪些子树独立并行,以及中间结果是否物化。同一等值连接可以按一个键分区,也可以按多个键组合分区;组合键也许倾斜更小,却可能让后续按单键聚合多一次 Exchange。
分布应被当成物理属性,地位类似排序顺序。若一个算子的输入不具备所需分布,就插入 Exchange;若已有共置、范围顺序或广播副本,就把它继续向后传递,尽量复用。实际优化通常先限制候选空间,再做代价比较,否则枚举成本本身会变得不可接受。
资源消耗模型会累计 CPU、I/O 和网络字节,适合控制集群总成本。响应时间模型更关心关键路径。CPU 与 I/O 能重叠时,一个阶段的时间更接近两者最大值,而不是简单相加;并行阶段的完成时间取决于最慢执行单元,还要加入任务启动、队列、网络拥塞和倾斜。
可用统计包括分区大小、不同值数量、直方图、高频值计数、压缩率和历史运行反馈。估计并不可靠时,运行时自适应很重要:根据实际行数调整广播决策、拆分热点任务或改变后续阶段并行度。
每个算子的并行度应由输入规模、可用核心、内存预算、网络带宽和系统并发共同决定。并行度太低会留下空闲资源,太高会产生大量小任务、连接和缓冲区,造成启动开销、内存碎片和资源争用。一个实用判断是比较每个任务的有效工作时间与固定开销:如果任务只运行几毫秒,却要建立通道、分配缓冲和汇报状态,继续拆分没有意义。
资源组与准入控制同样属于优化。单条查询的最快计划可能占满集群,但在多人环境下,它会让总体延迟恶化。系统可以限制每个查询的最大并行度,为短查询保留容量,并根据负载动态收缩或扩张 worker 数。
不要只用“CPU 使用率越高越好”评价并行计划。CPU 满载可能来自有效计算,也可能来自序列化、锁竞争和反复重哈希。应同时观察扫描字节、Exchange 字节、各任务输入分布、峰值内存、溢写量与尾部任务时间。
在少量节点上运行几秒钟的查询,节点失败后整条重跑可能可以接受。若查询跨数千个执行单元运行数小时,期间出现一次故障并不罕见;每次都从头重跑,甚至可能一直无法完成。更稳妥的目标是只重做失败节点负责的工作。
一种做法是在阶段边界物化输出。上游每个任务独立产生可定位的分片,下游确认所需分片齐全后再计算。某个上游任务失败,只需在其他节点重算那一个分片;下游任务未完成时也只重跑自身。最终结果写入有副本的持久存储。代价是阶段间不能完全流水线,中间结果写盘和等待屏障会增加延迟。
另一种做法记录数据集的生成关系。丢失分区时,根据 lineage 重新执行生成该分区所需的上游步骤;重要或重算昂贵的数据还可以检查点或复制。流水线容错更复杂:接收端要知道已经消费到哪个输入批次,源任务重跑后还要识别重复数据,确保结果不会多算或少算。
节点没有失败但运行异常缓慢时,它就是 straggler。等待它会拉长尾部时间。调度器可在其他节点推测执行同一任务,让两个副本竞速,先完成者生效,另一份取消。推测执行应接近任务尾部再启用,否则会把正常长任务误判为慢任务,白白消耗双倍资源。
副本位置也要考虑故障后的负载。若节点 A 的所有备份都集中在节点 B,A 故障后 B 会突然承担双倍工作。把不同分片的备份分散到多个节点和机架,接管流量才不会制造新的热点。
调优并行查询时,可以按一条稳定链路排查:先看各阶段实际输入与输出行数,再看 Exchange 字节和网络等待;接着看任务时长分位数、最大分片与中位分片的比值;然后检查内存峰值、哈希表溢写和本地 I/O;最后再调整分区键、广播阈值、批大小和并行度。
并行数据库的完成标准不是“用了很多节点”,而是数据分布、算子分解、通信、调度和恢复形成闭环。理想设计让大部分工作在本地完成,把不可避免的 Exchange 放在真正改变分布的边界上,并为倾斜和故障保留动态调整空间。