
最近在帮客户做一个数据库迁移的预研评估应用层几乎零改造就能把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 机器与部署形态测试环境我选了一台物理机规格如下CPU2颗Intel Xeon 6330单颗28核内存256GB存储NVMe SSD RAID10操作系统CentOS 7.9数据库AOracle 19c19.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核心逻辑对应业务AORDER BY order_no无索引可用全量数据排序导出、历史归档BORDER BY id主键可能走索引预排序按主键翻页、列表浏览CORDER BY region_id, amount DESC多条件组合排序报表DROW_NUMBER() OVER (PARTITION BY region_id ORDER BY amount DESC)分组Top-N、排名分析EORDER 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.8s18.6s约1.9倍D 窗口函数13.2s28.3s约2.1倍场景Oracle参数对齐崖山参数对齐倍数A 全表排序9.8s14.6s约1.5倍B 主键预排序1.2s1.4s约1.2倍C 复合列排序7.4s10.9s约1.5倍D 窗口函数13.2s19.9s约1.5倍E 分页取TOP 1000.38s0.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 ROWNUM100”会走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_id1 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的问题实际上七成功夫花在“怎么让测试公平”上。参数对齐、数据一致、结果校验、缓存清理任何一步偷懒结论都可能反过来。如果只记住一句话那就是先对齐参数再谈性能差异先校验结果再谈执行时间。