☰
YashanDB大数据量处理优化:批量提交、并行度、索引与MERGE实战
2026/10/19 9:12:23 网站建设 项目流程

1. 从一次慢查询排查说起

先说我自己的经历。有段时间我在某项目里做数据清洗,每天要处理几千万行的日志表,最初用的是几个零散的SQL脚本,跑一次全量清洗大概要四十分钟。后来业务侧要求把每天的调度窗口压缩到十五分钟以内,否则凌晨的计算任务会跟白天的报表查询打架。被逼着优化了一轮,核心手段其实特别朴素:批量提交、并行度控制、索引策略,再加上一条改写后的MERGE语句。最终清洗时长从四十分钟压到了九分钟,效果立竿见影。

这里面的工具就是YashanDB。可能有朋友还不熟悉它,简单说,这是一款国产的分布式关系型数据库,兼容Oracle的不少语法习惯,同时也在往MySQL生态上靠。它最打动我的点是,你不需要为了性能去改业务逻辑,大多数时候只要调整SQL写法、参数配置、提交策略,就能拿到很明显的收益。

这篇文章我不会讲太多理论,重点是把我在项目里实际用过、验证过有效的5个技巧拆开揉碎,每一步都告诉你当时是怎么想的、踩过什么坑、最后怎么调的。无论你是刚接触YashanDB,还是已经在生产环境里跑了一段时间,这几个技巧应该都能直接抄作业。

2. 批量提交技巧:别让事务成为隐形的性能杀手

2.1 单行提交为什么慢

很多从Oracle或者MySQL转过来的同学,写数据入库时习惯用一个循环,每处理一条就执行一次INSERT,然后马上COMMIT。在小数据量下这么写没毛病,但一旦单次任务要写入几百万行,这个写法的代价就非常明显。

问题出在事务提交本身。每次COMMIT都会触发一次日志落盘、锁释放、事务状态更新,这些操作的CPU和IO开销是固定的。你写一万条数据,就要付一万次固定开销,这个开销跟数据本身关系不大,纯粹是“次数税”。我见过一个极端例子,某同学用循环单条插入的方式导入两百万行数据,跑了将近两个小时,后来我把循环去掉,改成一次性攒够五千条再提交,同样的数据不到三分钟就完事了。差距就是这么夸张。

在YashanDB里,事务提交的机制跟主流数据库类似,每次提交都需要把重做日志刷到磁盘,确保事务的持久性。所以减少提交次数,本质上就是减少磁盘同步的次数。磁盘同步是毫秒级的操作,单看一次不觉得慢,乘上一万次、十万次,就成了灾难。

2.2 合理的批量尺寸怎么定

那是不是一次攒的越多越好?也不是。事务太大,会带来两个新问题:一是事务长时间持有锁,其他会话的读写会被阻塞;二是如果中途失败回滚,回滚的代价也会很大,因为undo信息同样要管理。

我常用的经验值是每批次两千到一万行之间,具体要看单行大小。行的平均长度越大,批次就应该越小。举个例子,如果一行只有几十个字节,比如纯ID和时间戳,那一万行一次提交是没问题的;如果一行里还有大字段,比如JSON、TEXT甚至BLOB,那两千行左右就要提交一次,避免单事务占用内存过多。

实际操作时,我会先按五千行跑一遍,观察监控面板上的事务耗时和锁等待。如果锁等待明显增加,就往小调;如果IO还比较空闲,就往大调。这个调参的过程不难,但一定要有意识去做,不要想当然选一个固定值就再也不管。

2.3 实操示例:用数组绑定代替单行循环

YashanDB的JDBC驱动支持标准的批量提交接口。以前我写数据是这样:

for (Record r : records) { PreparedStatement ps = conn.prepareStatement("INSERT INTO t VALUES (?, ?)"); ps.setLong(1, r.getId()); ps.setString(2, r.getName()); ps.executeUpdate(); conn.commit(); }

这个写法本身没什么语法错误,但性能上非常吃亏。后来改成:

conn.setAutoCommit(false); PreparedStatement ps = conn.prepareStatement("INSERT INTO t VALUES (?, ?)"); int count = 0; for (Record r : records) { ps.setLong(1, r.getId()); ps.setString(2, r.getName()); ps.addBatch(); count++; if (count % 5000 == 0) { ps.executeBatch(); conn.commit(); } } ps.executeBatch(); conn.commit();

关键改动是两个:一是把自动提交关掉,自己控制提交节奏;二是用addBatch把多条INSERT攒进同一个批量操作,一次网络往返发给数据库。这样既减少了提交次数,也减少了网络交互次数,两方面的开销都降下来了。很多新手容易忽略一个细节:批量提交之后,PreparedStatement里的参数并不会自动清空,所以下一次循环前必须重新set值,否则会出现数据重复或脏数据。我第一次写批量提交时就在这里栽过跟头,整批数据错位,排查了好久才发现是参数没刷新。

3. 并行度与负载控制:让压满的CPU喘口气

3.1 并行不是万能药

一提到提高数据处理效率,很多人第一反应就是“上并行”。在YashanDB里确实有并行查询和并行DML的能力,但并行是不是开得越大越好?答案是否定的。

我做过一个测试:一张千万行的表,做一次全表更新。并行度设为4时,耗时大约40秒;并行度调到8,耗时不降反升,来到55秒;调到16,直接涨到80秒。原因并不复杂,并行度上去了,每个并行服务进程都要分配内存、上下文切换开销增加,同时磁盘IO带宽是固定的,多路并行争抢同一块磁盘,反而互相拖慢。

所以并行度要用,但必须结合机器资源来定。我个人的经验是:如果数据库服务器是16核、64GB内存的配置,并行度控制在4到6之间比较稳妥;如果是32核以上的机器,可以试探性给到8,但一定要观察CPU使用率。如果发现CPU已经接近100%而且磁盘等待在上升,那说明并行度给大了,应该往回压。

3.2 YashanDB里怎么控制并行

YashanDB支持通过Hint来控制语句级别的并行,也可以在会话级别设置并行参数。我用得比较多的是后者,因为一条清洗任务通常包含多条SQL,逐个加Hint太繁琐。

会话级别设置并行度的方式大致如下:

ALTER SESSION SET parallel_degree_policy = MANUAL; ALTER SESSION SET parallel_degree = 4;

这样设置之后,当前会话里的查询和DML操作,只要优化器认为可以并行,就会尽量按4路并行来执行。

不过要注意,并行DML是有前提条件的,并不是任何一条UPDATE都能自动并行。如果目标表上有未提交事务、或者表结构比较复杂(如包含某些特殊索引或触发器),优化器可能会放弃并行执行计划,这时候就需要手动加Hint来干预。我不止一次遇到“明明设置了并行度,执行计划却显示串行”的情况,排查到最后都是因为表上有触发器。

3.3 结合批量提交使用的实测效果

在我的日志清洗场景里,我会先把批量提交做好,再叠加并行。具体做法是:把大表按ID区间拆成四个分片,每个分片单独起一个会话,每个会话里再按五千行一个批次提交。这样既享受了并行带来的吞吐提升,又不会因为单个事务过大而拖垮系统。

实测下来,单会话批量提交的写入速度大约是每秒7万行,开了4个会话并行处理后,总速度并没有线性变成每秒28万行,而是稳定在每秒18万行左右。这个数字说明一个问题:并行确实有用,但系统的瓶颈逐渐转移到了别的环节,比如磁盘IO和网络带宽。所以并行不是银弹,它是在其他瓶颈都被优化掉之后,才能发挥最大作用的加速手段。

4. 索引策略优化:为大数据量查询装上导航

4.1 索引不是越多越好

很多刚接触数据库优化的同学,一遇到查询慢,第一反应就是“给查询涉及的字段都建上索引”。这个思路有道理,但实践中很容易失控。索引本质上是额外的存储结构和额外的写入开销。你每插入一行数据,除了写表,还要同步更新所有相关索引,索引多了,数据写入速度会被明显拖累。

我在一个清洗任务里就有过教训。目标表本来有三个索引,写入速度还行。后来为了一个报表查询,又加了两个复合索引,结果数据导入环节慢了将近40%。加索引的本意是让查询快一点,结果因为导入是整个环节的瓶颈,反而拖累了整体效率。

所以在建索引之前,先要搞清楚这张表是读多写少还是写多读少。如果是清洗、导入这类重写场景,索引要克制;如果是查询报表类场景,索引就需要精心设计。不要一上来就追求“每个查询字段都有索引”,要基于实际SQL模式来设计。

4.2 复合索引的字段顺序

YashanDB的索引机制遵循最左前缀原则,也就是说,复合索引中字段的排列顺序,直接决定了这个索引能被哪些查询用到。很多人在这里容易犯迷糊,觉得反正都建了复合索引,查询条件随便写都能走索引。实际上并不完全是这样的。

我的经验法则是:区分度高的字段放前面,等值查询的字段优先于范围查询的字段。举个例子,如果查询条件是WHERE status = ? AND create_time > ?,那复合索引应该设计成(status, create_time),而不是(create_time, status)。因为status是等值查询,区分度高,放前面能让索引快速定位到一小部分数据,再在局部范围里用create_time做范围过滤,效率会高很多。反过来,如果把create_time放前面,索引要先按时间扫一大段,再在结果里过滤status,效率就差不少。

4.3 覆盖索引的隐性收益

覆盖索引是我在YashanDB里用得最多,但最容易被忽略的一个技巧。所谓覆盖索引,就是查询需要的所有字段都能从索引本身拿到,不需要回表读取数据行。对于高频的统计类查询,这个收益非常可观。

比如某个报表要统计每种状态的数据量:

SELECT status, COUNT(*) FROM orders GROUP BY status;

如果只在status上建普通索引,执行时可能会遍历索引后回表取数据,再聚合。如果建立(status, id)的复合索引(或者包含其他查询需要的字段),那查询引擎可以直接在索引里完成扫描和聚合,省掉回表这一步,耗时往往能下降一个量级。

当然我刚也说了,索引是有代价的。所以覆盖索引只建议加在真正高频的查询上,不要为了覆盖而覆盖。我的做法是:先跑一段时间的慢查询日志,把TOP 10高频SQL拎出来,逐个分析它们的访问路径,再决定要不要加覆盖索引。这样加索引,才是有依据的优化,而不是拍脑袋。

5. MERGE语句的妙用:一条语句完成比对更新与插入

5.1 传统写法的痛点

数据清洗场景里,经常要做这样一件事:如果某条记录在目标表里存在,就更新它;如果不存在,就插入。以前我习惯分两步走,先查一遍存在与否,再决定是UPDATE还是INSERT。这个写法在数据量小的时候问题不大,数据量一大,问题就来了。

首先,每次判断存在性都要走一次查询,几千几万条数据就要查几千几万次,开销非常可观。其次,查询和更新之间有时间差,如果数据源本身在变,可能出现判断时不存在、插入时却已经存在的竞态问题,导致唯一约束冲突。这些问题在YashanDB里都可以用MERGE语句来解决。

5.2 MERGE的核心语法

YashanDB的MERGE语法跟Oracle的用法非常接近,基本的形态是这样的:

MERGE INTO target_table t USING source_table s ON (t.id = s.id) WHEN MATCHED THEN UPDATE SET t.name = s.name, t.update_time = SYSDATE WHEN NOT MATCHED THEN INSERT (id, name, update_time) VALUES (s.id, s.name, SYSDATE);

这条语句的逻辑很清晰:根据ON条件判断目标表和源表中哪些记录匹配,匹配了就更新,没匹配就插入。整个过程是一次操作,数据库内部完成判断和写入,不需要应用层反复查询。

我在实际项目中,经常把source_table换成子查询,而不是一个物理表。比如先JOIN两张临时表,再对结果做MERGE,灵活性非常高。这里有几个细节值得注意:

第一,ON条件里的字段一定要能唯一标识一条记录,通常用主键或业务唯一键。如果ON条件有多条匹配,MERGE会报错,因为数据库不知道该更新哪一条。

第二,WHEN MATCHED分支的UPDATE可以只更新部分字段,不用把整行都覆盖。这样能减少不必要的写入开销。

第三,如果源表里有重复记录,也会导致MERGE失败。所以执行MERGE之前,最好先确认源数据是去重后的,这是一个典型的坑。

5.3 用MERGE替代“先查后写”实测对比

我做过一次小实验,数据量是一百万条记录,一部分需要更新,一部分需要插入。传统“先查后写”的方式,总共跑了大约7分钟;用MERGE一次性完成,耗时大约2分半。差距不是一点半点,主要省下来的就是反复查询的开销。

而且MERGE还有一个隐性优点:它的事务边界非常清晰。传统方式里,如果更新成功了一批、插入的时候失败了,你可能要自己去协调哪些数据已经处理过,哪些没有。MERGE把更新和插入放在同一条语句里,要么全部成功,要么全部回滚,状态管理简单可靠。这也是为什么我在规整上游数据、做增量同步这类任务里,特别偏爱MERGE的原因。

6. 临时表与中途落盘的取舍:让清洗过程更可控

6.1 什么时候该用临时表

做数据处理时,很多人喜欢一口气用大SQL把所有逻辑写完:表A关联表B,再跟表C做聚合,再嵌套一层子查询,最后更新到目标表。这种写法看起来很“高级”,但实际执行时,数据库优化器可能会选择一种并不理想的执行计划,比如重复扫描同一张大表,或者生成的中间结果集大到内存都放不下,被迫频繁落盘。

YashanDB支持会话级临时表和全局临时表。临时表的语义是,数据生命周期只在当前会话或当前事务内,不需要手动清理,会话结束自动消失。合理使用临时表,可以把复杂逻辑拆成若干个简单步骤,每一步的结果物化到临时表里,这样不仅执行计划更可控,中间过程也更容易排查。

我通常会把临时表用在两类场景。一类是数据清洗的中间结果:比如先把源数据标准化、去重,写入临时表,再拿临时表去JOIN目标表做更新。这样JOIN的时候,源数据已经是干净、紧凑的,关联效率要高不少。另一类是复杂聚合的中间层:比如先按月聚合到临时表,再基于临时表做周聚合或年聚合,避免在一条SQL里做多层嵌套。

6.2 临时表的使用技巧与注意事项

在YashanDB里,临时表的定义方式跟普通表很相似,但有一个重要区别:临时表的数据是会话级或事务级的,不会被其他会话直接看到。我在清洗任务里的用法通常是:

CREATE GLOBAL TEMPORARY TABLE tmp_clean_data ( id NUMBER, name VARCHAR2(100), status NUMBER, create_time DATE ) ON COMMIT PRESERVE ROWS;

ON COMMIT PRESERVE ROWS表示提交事务后数据仍然保留,适合在一个会话里先写入、再多次查询的场景。如果你希望事务提交后数据自动清空,可以用ON COMMIT DELETE ROWS。

使用临时表时,我发现两个容易踩的坑。第一个是临时表上不要建立过多索引,临时表本身数据量通常不会特别大,而且生命周期很短,建索引反而浪费时间和空间。第二个是临时表的统计信息不一定准确,因为创建时是空的,写入数据后统计信息不会自动更新,可能会影响优化器的判断。如果发现用了临时表之后执行计划反而更差,可以手动收集一下统计信息再跑。

6.3 中途落盘:内存与磁盘的博弈

有时候,我会有意让中间结果落盘,而不是一直在内存里连续计算。这个想法看起来跟“追求高效”有点矛盾,但实际操作中有它的道理。

YashanDB在处理超大JOIN或聚合时,如果内存不够,本身就会把中间结果写到磁盘临时文件。与其让数据库自主决定什么时候落盘、以什么格式落盘,不如我在关键步骤主动物化一份临时表。这样做的优势是:第一,中间结果可以被复用,避免同一个子查询被反复执行多次;第二,我可以显式地在临时表上做分区、采样或者加索引,精细控制后续步骤的性能。

当然,临时表也不是用得越多越好。每落盘一次,就多一次写磁盘、读磁盘的开销。所以这里要有一个度:如果中间结果只有几千行,完全没必要建临时表,直接用子查询就好;如果中间结果有几百万行、而且后续要被复用两三次,那临时表的收益就很明显了。

7. 常见问题与排查技巧实录

7.1 批量提交后数据重复或错位

我在一开始做批量提交时就遇到过这个问题,而且现象很隐蔽:数据的总行数是对的,但里面的个别字段串了。排查到最后,发现是PreparedStatement在批量提交后没有重新绑定参数。因为同一批数据里的参数值如果跟上一批不一样,而你没有重新set,数据库就沿用上一批的值,导致数据错位。

这个问题的排查思路是:每次进入循环时,先对所有占位符重新set一遍,哪怕值跟上一批一样,也务必重新赋值。另外,在执行executeBatch之后,可以调用clearParameters清空参数,再进入下一批。这样做虽然多几行代码,但能彻底杜绝这种隐蔽的数据错乱。

7.2 MERGE执行报错:源表重复数据导致匹配歧义

MERGE语句报ORA-30930或类似的“行过多”错误,十有八九是因为源数据里有重复的关联键。比如你用订单号做ON条件,但源表里同一个订单号出现了两行,数据库就不知道到底该把哪一行更新到目标表。

解决办法是在MERGE之前做一次去重。常见做法是用ROW_NUMBER()窗口函数给源数据编号,只保留编号为1的行,再执行MERGE。我当时排查这个问题的速度还算快,因为错误信息比较明显,但如果你没往“重复数据”方向想,可能会浪费很多时间。建议在写MERGE任务的代码时,永远把去重步骤放在前面。

7.3 并行度设置了但没生效

前面讲过,并行DML在某些条件下会被优化器放弃。最常见的四个原因是:

第一,目标表上有触发器,并行更新会被禁止;第二,目标表上有未提交事务,并行操作需要等待;第三,会话级参数被语句级Hint覆盖;第四,表本身太小,优化器认为并行不值得。

我在排查时,一般先看执行计划,EXPLAIN PLAN FOR加目标SQL,确认计划里有没有PX(Parallel eXchange)字样。如果没有,再去查表上有没有触发器或外键约束。这个思路基本能覆盖绝大多数“并行不生效”的情况。

7.4 临时表统计信息过期导致执行计划偏差

临时表数据是动态写入的,统计信息很难自动维护。有时候第一天任务跑得很快,第二天同样的任务突然变慢,很可能就是因为临时表里的数据量变化了,而执行计划还是按旧的统计信息生成的。

我的处理方式是:在写入临时表之后、正式执行查询之前,手动收集一次统计信息。YashanDB支持DBMS_STATS包,用法跟Oracle的非常接近:

EXEC DBMS_STATS.GATHER_TABLE_STATS('用户名', '临时表名');

这一步操作虽然会花一点点时间,但能有效避免执行计划跑偏。

8. 个人实操心得

这几个技巧单独拎出来,每一个都不算高深,但组合在一起,确实能改变数据处理环节的整体体验。我最深的感受是,数据库优化的核心不是某一个神奇参数,而是一套“观察-假设-验证”的循环:先观察瓶颈在哪,再针对瓶颈做调整,最后用数据验证调整的效果。批量提交解决的是固定开销问题,索引策略解决的是查询路径问题,MERGE解决的是业务逻辑冗余问题,临时表解决的是执行计划失控问题,并行度解决的是资源利用率问题。把它们按顺序排好,基本上可以覆盖绝大多数数据处理场景的优化空间。

最后再分享一个小技巧。每次做优化之前,我都会先记录优化前的耗时和系统负载,优化后做对比。这不是形式主义,而是因为数据库优化的收益很可能被其他因素掩盖。比如你明明优化了SQL,但当时IO本身就有波动,测出来的提升就不准。先有一组基线数据,再来判断优化是否真的有效,这是所有经验里最值得养成的一个习惯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询