最近在帮客户做一个数据库迁移的预研评估,应用层几乎零改造就能把SQL从Oracle挪到崖山上跑,当时我们都挺乐观。结果一压测就露了馅——夜间跑批里那些大排序SQL,执行时间直接比原库翻了一倍多。这个反差让我决定把“排序”这件事单独拎出来,认认真真做一次Oracle和崖山的排序性能对比测试。
这篇文章不是跑个TPC-H给个总分就完事,而是从测试环境怎么搭才公平、排序SQL怎么设计才贴近生产、参数配置对结果的影响有多大、以及我踩过的几个能把结论彻底带偏的坑,完整记录一遍。如果你也在做从Oracle到崖山(或者类似数据库)的迁移评估、选型调研,或者只是想知道这两个数据库在排序场景下到底差多少,这篇文章应该能帮到你。
1. 为什么拿排序当迁移试金石
1.1 排序算子在SQL执行链路里的地位
大多数业务开发平时感知不到排序的存在,但它几乎无处不在。GROUP BY要排序,DISTINCT要排序,UNION要排序,窗口函数要排序,ORDER BY更是直白。连优化器在做连接时,都可能因为某个输入已经有序而选择排序合并连接(Sort Merge Join)。可以说,排序是SQL执行链路里最基础的算子之一,也是跑批、报表、统计分析这些重活绕不开的坎。
排序这个算子的特殊之处在于,它同时压榨三个维度的资源:CPU要反复执行比较器,内存要容纳排序区(work area),一旦内存放不下就会溢出到临时表空间,把磁盘IO也拉下水。一个排序跑得慢,你很难简单归因于“CPU差”或者“磁盘慢”,往往是内存管理策略、临时空间IO路径、并行调度算法共同作用的结果。这恰恰是不同数据库执行引擎差异最容易暴露的地方。
我做过的很多对比测试里,简单主键点查、小数据量过滤这种场景,Oracle和崖山几乎看不出区别;但排序一旦上规模,内存够不够用、临时空间写入优不优化、优化器对排序列的选择性估算准不准,全都现出原形。所以迁移预研阶段,我习惯把“排序”作为算子级压测的第一站,而不是只跑业务用例。
1.2 功能兼容不能代替性能验证
崖山的SQL语法、数据类型、PL/SQL和Oracle高度兼容,这是它能承接Oracle应用的基础。但“语法能跑”和“生产能扛”是两回事,这个认知差距在迁移项目里付出的代价最大。
一个ORDER BY在数据量小的时候谁都快,一旦到千万行级别,排序区大小、临时空间策略、优化器对排序列的选择性估算就开始决定成败。很多迁移项目在前期POC阶段只做功能验证,跑几个业务用例看到结果一致就签字了,上线后才发现批量任务全挂在排序统计上。我这次专门把排序拿出来做算子级压测,就是不想让这种隐患留到上线之后。
2. 测试环境搭建:公平性比跑分本身更难
2.1 机器与部署形态
测试环境我选了一台物理机,规格如下:
- CPU:2颗Intel Xeon 6330(单颗28核)
- 内存:256GB
- 存储:NVMe SSD RAID10
- 操作系统:CentOS 7.9
- 数据库A:Oracle 19c(19.16)
- 数据库B:崖山YashanDB 22.2
两个数据库装在同一台机器上,测试时轮流启停,只跑其中一个。为什么不直接用两台“配置一样”的机器?因为“配置一样”很难真正做到,CPU主频、SSD磨损、固件版本都可能有细微差别,单机双实例轮流跑是控制变量最干净的办法。它也有代价:两个库需要共享同一份硬件资源,所以测试时我会先把另一个实例完全停掉,避免后台进程抢占CPU和IO。
还有一个小细节:Oracle的临时表空间和崖山的临时数据文件,我分别放在不同的目录上,避免两个库的临时IO路径在物理层互相干扰。虽然轮流跑本来就不会同时读写,但分开存放能减少文件系统层面的碎片化影响。
2.2 数据一致性与参数对齐
数据一致性是测试的前提,这一点上没有商量余地。我先用Python脚本生成一份CSV文件,大约1.6GB,然后分别用Oracle的SQL*Loader和崖山的导入工具加载进去,保证两张表的数据完全一致。很多测试喜欢在两边“各自随机生成数据”,这是大忌——两边数据分布不一样,排序成本和结果都不一样,时间对比就失去了意义。
参数对齐是另一个重头戏。两边都按256GB内存的常规生产配置来调,排序区、内存上限、并行度等都尽量拉到同一水位。这里我先卖个关子:刚开始我故意用两边各自的默认参数跑了一轮,结果差距大得吓人;后来把参数对齐再跑一轮,结论完全不同。这一块的细节我在第5章专门展开。
统计信息也要处理好。Oracle里用DBMS_STATS收集,崖山用对应的统计信息采集命令,目标都是让优化器能看到完整的数据分布,避免因为“没统计信息”给出偏差计划。全部准备完后,我会先跑几条基础SQL确认两边返回结果一致,再开始正式的排序压测。
3. 排序测试SQL设计:五类场景覆盖生产形态
3.1 测试表结构与数据构造
测试表设计成生产订单表的形态:
CREATE TABLE sort_test ( id NUMBER(12) NOT NULL PRIMARY KEY, order_no VARCHAR2(32), customer_name VARCHAR2(64), region_id NUMBER(6), amount NUMBER(12,2), status_cd CHAR(1), created_at DATE );一共灌入1000万行数据。order_no是32位随机字符串,包含大小写字母和数字;customer_name用中文姓名字典随机构造;region_id有倾斜,约30%落在region 1,其余分散到其他区域;amount是偏态分布,少数大额订单占了大头;status_cd只有0/1/2三个值;created_at在最近两年内均匀分布。
这样设计不是随意拍脑袋:生产环境里的订单表就是这种“宽度不小、字符串列多、分布有倾斜”的形态。排序时每一行的宽度会影响比较器和内存占用,倾斜分布会影响分区排序的负载均衡,偏态金额会影响排序过程中的数据重排成本。用均匀随机数据测出来的结果,往往比真实场景乐观得多——这一点我在第6章的踩坑记录里还会详细说。
3.2 五个场景分别覆盖哪类业务
我把排序场景拆成五类,每类都对应生产里真实存在的SQL形态:
| 场景 | SQL核心逻辑 | 对应业务 |
|---|---|---|
| A | ORDER BY order_no(无索引可用) | 全量数据排序导出、历史归档 |
| B | ORDER BY id(主键,可能走索引预排序) | 按主键翻页、列表浏览 |
| C | ORDER BY region_id, amount DESC | 多条件组合排序报表 |
| D | ROW_NUMBER() OVER (PARTITION BY region_id ORDER BY amount DESC) | 分组Top-N、排名分析 |
| E | ORDER BY id + ROWNUM分页取前100条 | 业务系统最常见的翻页接口 |
这里我想专门提醒一下场景D。窗口函数做分区排序在SQL里写起来只有一行,但执行层面的开销远比看起来大:数据库要把数据按region_id分完区,再在每个分区内做一次排序,分区内存放不下就溢出。优化器几乎没有办法用索引消除这类排序,只能实打实算。生产系统里凡是“每个客户取最近一笔订单”“每个区域取销量前10”这种需求,背后都是这个场景。它也是这次测试里最能拉开差距的场景之一。
3.3 结果校验:只比时间是不够的
执行时间只是半个结果,排序对了才有意义。我另外做了两层校验:
第一层是排序结果正确性校验。两边SQL跑完后,把排序列加主键拼接成一个字符串,在数据库侧做MD5聚合,比对两边算出来的MD5是否一致;不一致时再导出前10万行做逐行diff,定位差异原因。这一步看着麻烦,但非常必要——数据库的排序规则、NULL位置、字符串比较方式稍有不同,排序后的顺序就会不一样,时间再快也没用。
第二层是执行计划校验。每次跑之前先EXPLAIN看一遍计划,确认两边的执行形态是可比的,比如都是SORT ORDER BY,而不是一方碰巧走了索引。如果计划形态不同,我不会直接比时间,而是先搞清楚计划差异是怎么来的,再决定是调整参数还是改写SQL,让测试回到同一赛道。
4. 实测数据:差距到底暴露在哪里
4.1 两轮测试的数据对比
我把测试分成两轮:第一轮两边都用彼此默认的参数,第二轮把内存相关参数对齐后再跑。这里直接上数据(每场景预热后跑5次取中位数):
| 场景 | Oracle(默认) | 崖山(默认) | 倍数 |
|---|---|---|---|
| A 全表排序 | 9.8s | 18.6s | 约1.9倍 |
| D 窗口函数 | 13.2s | 28.3s | 约2.1倍 |
| 场景 | Oracle(参数对齐) | 崖山(参数对齐) | 倍数 |
|---|---|---|---|
| A 全表排序 | 9.8s | 14.6s | 约1.5倍 |
| B 主键预排序 | 1.2s | 1.4s | 约1.2倍 |
| C 复合列排序 | 7.4s | 10.9s | 约1.5倍 |
| D 窗口函数 | 13.2s | 19.9s | 约1.5倍 |
| E 分页取TOP 100 | 0.38s | 0.46s | 约1.2倍 |
为什么两轮结果差这么多?因为Oracle默认靠PGA自动管理能分到足够多的排序内存,而崖山默认的work area给得偏小,大排序直接掉进磁盘排序。磁盘排序一启动,IO就成了瓶颈,时间自然翻倍。等我把两边的排序内存上限调齐,崖山的耗时立刻收敛到Oracle的1.5倍左右。
这里必须坦白一句:具体数字只代表我这套测试环境,不同机器、不同数据分布,倍数会有浮动,但相对关系大概率是稳定的——全表排序和窗口函数是最敏感的场景,索引预排序和分页这类有捷径的场景基本拉不开。
4.2 执行计划形态对比
看执行计划是理解差距的关键入口。Oracle的SORT ORDER BY、WINDOW SORT、INDEX FULL SCAN这些算子,在崖山的执行计划里都有对应形态,算子名字也几乎一样,说明崖山的优化器框架整体上是贴着Oracle的一套思路做的。但“计划长得像”不代表“算子实现一样”——差距藏在内存管理、临时空间写入路径和数据比较器的底层实现里。
用场景E举个例子:Oracle 19c对“ORDER BY id + ROWNUM<=100”会走COUNT STOPKEY加INDEX FULL SCAN,本质是Top-N优化,不用把整表排完。崖山的计划也是类似形态,所以两边都很快。但一旦把排序列换成没有索引的普通列,两边都会老老实实做全表排序,时间的差距立刻拉大。这说明测试排序性能时,选什么列排序、有没有索引可用,直接决定了你测的是“优化器捷径”还是“硬排序能力”。
5. 参数黑洞:排序性能差距的半壁江山
5.1 排序内存:内存排序还是磁盘排序
排序能不能在内存里完成,是影响性能的第一因素,没有之一。Oracle的PGA_AGGREGATE_TARGET体系会自动管理工作区大小,你给足PGA,大排序会优先在内存做;崖山也有类似的工作区机制,但默认策略偏保守,给排序的内存上限明显更小。
我做了一组对照实验:把崖山的排序工作区从默认值调到和Oracle的PGA等效水位后,场景A的耗时从18.6秒降到14.6秒,提升超过20%。再把两边的排序区同时调小,强制都走磁盘排序,Oracle的耗时涨到16秒,崖山涨到21秒,差距反而缩小了。这说明一个反直觉的结论:两边都在内存排序时差距明显,都在磁盘排序时差距反而小——崖山的短板主要在内存排序的算子实现效率,而不是磁盘排序这部分。
所以做任何数据库排序对比,第一步必须确认两边“内存排序/磁盘排序”的边界是一致的。否则你测出来的其实是“内存排序 vs 磁盘排序”的差距,而不是数据库本身的排序能力差距。
5.2 并行度:排序到底能不能多线程干活
排序是个天然适合并行的操作,数据可以分片排序再合并。Oracle的并行排序调度很成熟,加/*+ PARALLEL(4) */后,大排序能明显提速。崖山也有并行能力,但我在实际测试中发现它对并行度的敏感性更强:4并行时场景A从14.6秒降到10秒左右,效果不错;但并行度调到8以后,协调开销反而吃掉了一部分收益,提升变得很有限。Oracle在同样的并行度下还能继续稳定提速。
这个差异对迁移的实际影响是:如果生产环境里的大排序SQL本来就用高并行度撑着,迁到崖山后同样并行度可能达不到预期的提速效果,需要额外关注。
5.3 统计信息与优化器:一个直方图的差距
排序测试里还有一个容易被忽略的变量:优化器对数据分布的认识。当ORDER BY列参与了WHERE过滤,比如WHERE region_id=1 ORDER BY amount,直方图有没有收集会直接改变执行计划的选择。
我专门做了个对照:统计信息完整时,Oracle和崖山的计划形态基本一致;但模拟生产环境“漏收集统计信息”的场景时,崖山更容易选错计划,把一个只返回1万行的查询做成全表排序。这提醒我们:迁移到崖山后,统计信息采集任务必须像在Oracle里一样严格执行,不能依赖数据导入时的默认统计,否则排序SQL的执行计划会随机漂移。
6. 踩坑记录:四个能把结论带偏的坑
6.1 字符串排序规则不一致
第一次做结果校验时,场景A的MD5就对不上。排查了很久才发现,不是排序算法问题,是排序规则问题。order_no是大小写字母加数字混合的随机字符串,Oracle默认BINARY排序,按ASCII码排,大写字母在小写字母前面;而崖山的默认字符串排序不是简单二进制,带了locale规则,导致同一批数据在两边排序后的顺序不一样。
解决方式是两边都用显式口径比较:我在SQL里把排序列统一套一层排序规则函数,或者干脆都转成小写后再排,确保比较逻辑完全一致。这个坑很隐蔽,因为你只看前几条数据可能觉得“差不多都对”,但翻到第10万条就开始错位。迁移后所有字符串列排序的列表接口,都要重点做这种校验。
6.2 NULL值排序位置不同
表里的amount字段本来就有空值,这部分是真实生产数据里常见的。Oracle升序ORDER BY amount时默认NULLS LAST,空值排最后;崖山跑同样的SQL,空值跑到了最前面。这问题如果没提前发现,迁移后所有前端列表页的分页顺序都会错位,用户翻页时会感到明显的数据“跳变”。
解决办法很简单:SQL里显式写NULLS LAST或NULLS FIRST,两边统一,不要依赖数据库默认行为。这也是一个经验——排序相关的SQL在迁移前应该做“排序口径清单”,把每个排序列的NULL规则明确下来。
6.3 数据倾斜:均匀数据会骗人
测试初期为了图省事,我用了完全均匀的随机数据,结果崖山的表观表现比后来用倾斜数据好看不少。原因是排序性能不仅看行数,还看重复值分布:场景D的分区排序里,如果某个region_id独占30%的数据,这个分区的排序就成了整条SQL的瓶颈,其他小分区排完了也要等它。
后来我把数据生成逻辑改成1:3:6的近似倾斜分布,结论立刻变保守,也更贴近生产。所以做排序压测时,数据分布一定得按生产的真实倾斜度来构造,均匀数据测出来的结果只能当参考,不能当决策依据。
6.4 热数据与“旧版本没删干净”的环境坑
排序结果受缓存影响极大,同一个SQL连续跑十次,后几次会明显变快。我采用的策略是:每轮测试前重启目标实例清缓存,每个场景跑5次取中位数,而不是取最小值。用最小值很容易被缓存虚高骗到,以为数据库“性能很好”,实际是热数据效应。
另外一个环境坑值得单独记一笔:测试机器之前装过一版旧数据库,卸载不干净,旧实例的监听和共享内存和新实例打架,导致某天的测试数据突然全部异常。排查下来才发现是旧进程没清干净,两个实例在同一批端口上互相干扰。最后把旧实例彻底清理,重新初始化环境才恢复稳定。装新库之前把老环境彻底清干净,这个步骤在测试环境搭建时必须严格执行。
7. 结论与迁移实操建议
7.1 排序性能结论怎么下
在参数对齐的前提下,崖山的排序性能大约是Oracle的0.65到0.8倍效率,也就是耗时约1.3到1.5倍,具体取决于场景。索引预排序、Top-N分页这类有优化器捷径的场景,两边差距很小;全表排序、窗口函数这类硬排序场景,崖山有明显差距,但远没到“不能用”的程度。
我也顺手用MySQL 8.0跑了同样的场景做参照:MySQL的全表硬排序耗时不比崖山慢,但分页翻深了以后表现明显更差。这说明每个数据库的排序能力各有短板,不能用“谁比谁强”一句话概括,必须落到具体SQL形态上去评估。
7.2 迁移前要做的事
基于这次测试的经验,我给正在做同类迁移评估的团队三条建议:
第一,把生产库的慢SQL清单拉出来,重点标出所有带ORDER BY、GROUP BY、DISTINCT、窗口函数的语句。这些是排序场景的潜在雷区,别等上线后再找。
第二,在测试环境把这类SQL全量跑一遍,参数对齐、结果校验、执行计划对比三件套都走完。确认哪些SQL在崖山上会劣化超过2倍,哪些基本持平,列成一张风险清单。
第三,对劣化严重的SQL优先做改写,不要硬扛。比如窗口函数拆成两次聚合,排序分页改成游标或按主键翻页,让崖山发挥它的优化器能力,而不是拿它的短板去硬碰硬。
7.3 最后的经验
个人最大的体会是:测试排序性能这件事,看起来就是跑几个SQL的问题,实际上七成功夫花在“怎么让测试公平”上。参数对齐、数据一致、结果校验、缓存清理,任何一步偷懒,结论都可能反过来。如果只记住一句话,那就是:先对齐参数,再谈性能差异;先校验结果,再谈执行时间。