千万级数据批量更新优化:UPDATE替换MERGE INTO
千万级数据批量更新优化:从慢到快,UPDATE 替换 MERGE INTO 实战
大家好,我是政乾,好久没更新博客了~
今天给大家带来一篇超实用的SQL 优化实战—— 针对业务中常见的多列关联批量更新场景,
手把手教你把低效UPDATE改成高性能MERGE INTO,新手也能看懂、直接复制复用!在实际开发中,
-------我们经常遇到这类需求:遇到需要根据“子表”的聚合结果来更新“主表”的场景。
- 比如:统计部门下员工的指标总和,更新到部门表;
- 统计订单商品的金额总和,更新到订单主表……
今天就以「更新妖灵之地部门的统计指标」为例,拆解原 UPDATE 写法的坑,详解MERGE INTO 优化方案,让你的批量更新效率直接起飞!
整体概览
文章目录
- 千万级数据批量更新优化:从慢到快,UPDATE 替换 MERGE INTO 实战
- 一、业务场景说明
- 二、优化前的 SQL 深度剖析(它到底慢在哪?)
- 核心问题:执行逻辑太坑(新手重点理解)
- 性能瓶颈 & 潜在风险
- 三、优化后:MERGE INTO 语句详解(新手逐行看懂)
- 核心逻辑拆解(新手必读!)
- 第一层:数据源构建(`USING (SELECT...) AS src`)
- 第二层:匹配条件(ON)
- 第三层:更新动作(WHEN MATCHED THEN UPDATE)
- 四、核心:为什么 MERGE INTO 完胜原 UPDATE?
- 底层优化逻辑:
- 五、使用注意事项 & 拓展建议
- 1. MERGE INTO 使用规范(新手必记)
- 2.不同数据库写法差异
- 3.额外优化建议
- 六、完结撒花!!
- 哈哈哈,如果感觉"有帮助",欢迎点赞,关注收藏哦!!!
一、业务场景说明
先明确我们的业务需求,简单直白:
- 有两张表:
departments(部门表)、employees(员工表); - 需要筛选出部门名称包含「妖灵之地]的部门;
- 统计这些部门下所有员工的
num3总和、num4总和(就是聚合结果); - 将统计结果批量更新到部门表的
num1、num2字段。
这是非常典型的主表 + 子表聚合关联批量更新,也是最容易写出低效SQL的场景之一!
二、优化前的 SQL 深度剖析(它到底慢在哪?)
先看优化前的原始
SQL,很多新手(甚至有基础的开发者)都这么写(包括我自己刚入行那会儿也是哈哈哈):
-- 优化前:低效关联更新UPDATEdepartments t1SET(t1.num1,t1.num2)=(SELECTsum(t2.num3)asnum1,sum(t2.num4)asnum2FROMemployees t2wheret1.dept_id=t2.dept_id)wheret1.dept_namelike'妖灵之地';核心问题:执行逻辑太坑(新手重点理解)
这条
SQL看似简单,执行逻辑完全是「灾难级」,我给大家拆解底层执行流程:扫描主表–>逐行触发子查询–>数据汇总与回写
- 先筛选部门表(扫描主表):数据库首先根据
WHERE t1.dept_name LIKE '妖灵之地',扫描departments表,筛选出符合条件的所有行(假设有100W行)。 - 逐行触发子查询:
- 逐行循环这
100W条部门数据,每一行都执行一次子查询SELECT SUM... WHERE t1.dept_id = t2.dept_id; - 每执行一次子查询,都要去员工表
employees做全表 / 索引扫描,计算 SUM;
- 逐行循环这
- 数据汇总与回写:子查询返回该部门的员工汇总数据,然后数据库将这行数据更新到主表。最终:
100W条部门数据 → 执行100W次子查询 → 扫描100W次员工表!
性能瓶颈 & 潜在风险
- 性能极差:“逐行”扫描的致命伤;员工表数据量越大(比如百万、千万级),执行越慢,直接拖垮数据库;
- 锁表风险:更新耗时过长,会长时间锁定部门表,导致其他业务无法操作,引发生产故障;
- 空值问题:如果部门没有员工,子查询返回 NULL,会把
num1/num2更新为 NULL,业务数据出错; - 扩展性差:后续加统计字段、加筛选条件,代码会越来越乱,可读性极差。
💡 新手提示:这种 **「相关子查询 + 逐行更新」的写法,是批量更新的大忌 **!数据量小的时候没感觉,数据量大直接卡死!
三、优化后:MERGE INTO 语句详解(新手逐行看懂)
针对上面的问题,我们用MERGE INTO优化,
这是
Oracle迁到GaussDB等国产数据库的批量关联更新神器!我们来先看最终优化语句:
-- 优化后:高性能 MERGE INTO 批量更新mergeintodepartments tusing(-- 第一步:先计算出需要更新的 部门ID + 统计值(只执行1次)selectdept_id,sum(s2.num3)sumnum1,sum(s2.num4)sumnum2from(-- 子查询 S1:先找出所有符合条件的部门ID-- 筛选目标部门:妖灵之地(先缩小数据范围)selectdept_idfromdepartmentswheredept_namelike'妖灵之地'groupbydept_id)s1-- 左连接员工表:避免无员工的部门被更新为NULLleftjoinemployees s2ons2.dept_id=s1.dept_idgroupbys1.dept_id)src-- 把统计结果作为「数据源」on(t.dept_id=src.dept_id)-- 关联条件:部门ID匹配whenmatchedthen-- 匹配成功就执行更新updatesett.num1=src.sumnum1,t.num2=src.sumnum2;核心逻辑拆解(新手必读!)
看起来复杂,其实就是纸老虎!! 我们把它拆成三层结构来看:
一定要看哦!也可以自己模拟下,相信我,绝对收获满满!!
第一层:数据源构建(USING (SELECT...) AS src)
这是整个优化的灵魂。我们将原本“逐行计算”的逻辑,转变为了“一次性构建一个虚拟结果集”。
子查询 S1:
SELECT dept_id FROM departments WHERE dept_name LIKE '妖灵之地' GROUP BY dept_id- 这一步先把所有符合条件的
部门 ID一次性取出来。即使有重复的dept_id,我们用GROUP BY去重。 - 新手提示:这里先做过滤,避免了后续关联庞大的
departments全表。
- 这一步先把所有符合条件的
主查询:
LEFT JOIN employees s2 ON s2.dept_id = s1.dept_id- 拿着刚才过滤好的少量部门 ID(比如 1000 个),去左连接
employees表。 - 补充:为什么用
LEFT JOIN而不是INNER JOIN?- 因为如果某个部门没有员工(右表无匹配),
LEFT JOIN会返回该部门 ID,对应的sumnum1和sumnum2为NULL。 - 但后续聚合
SUM(NULL)结果是NULL,我们再通过后续处理或NVL转为 0,可以避免将原有数据覆盖为NULL。(这个操作看你更改的原语句情况而定,结合业务)
- 因为如果某个部门没有员工(右表无匹配),
- 拿着刚才过滤好的少量部门 ID(比如 1000 个),去左连接
最终聚合:
GROUP BY s1.dept_id并计算SUM。- 这一步确保每个部门只返回一行汇总数据。
结论:整个USING部分,只执行了一次,就生成了所有需要更新的部门及其汇总数据。
第二层:匹配条件(ON)
ON(t.dept_id=src.dept_id)告诉数据库:当departments表的dept_id与源数据src中的dept_id相等时,执行更新操作。
第三层:更新动作(WHEN MATCHED THEN UPDATE)
WHENMATCHEDTHENUPDATESETt.num1=src.sumnum1,t.num2=src.sumnum2;一旦匹配上,就用源数据中的汇总值更新目标表的字段。
四、核心:为什么 MERGE INTO 完胜原 UPDATE?
给大家做直观对比,看完就知道优化的价值:
| 对比维度 | 原始 UPDATE | MERGE INTO |
|---|---|---|
| 执行次数 | 子查询执行 N 次(N = 部门数) | 数据源只执行 1 次 |
| 表扫描次数 | 扫描员工表 N 次 | 扫描部门表 + 员工表 各 1 次 |
| 空值风险 | 无员工会更新为 NULL | 通过LEFT JOIN+SUM,无数据时结果为 NULL,可通过 NVL(src.sumnum1, 0)灵活处理为 0,避免数据丢失。 |
| 执行效率 | 数据量大极慢 | 百万级数据秒级完成(重点!!) |
| 锁表时间 | 长,易故障 | 短,业务无影响 |
| 可读性 | 差,嵌套难维护 | 清晰,分层逻辑,易扩展 |
底层优化逻辑:
- 原始 UPDATE:行级循环更新(嵌套循环,时间复杂度 O (N));
- MERGE INTO:集合级批量更新(先算好所有结果,再一次性更新,时间复杂度 O (1))。
📌补充:
MERGE INTO本质是「有则更新、无则插入」,这里我们只用了更新功能,完全适配批量关联更新场景,是数据库官方推荐的优化方案!(本人亲自生产实操!!)
五、使用注意事项 & 拓展建议
1. MERGE INTO 使用规范(新手必记)
- 关联条件必须唯一:用主键 / 唯一键(如
dept_id),避免一对多导致更新异常; - 必须加筛选条件:在
using子查询中先缩小数据范围,不要全表更新; - 优先左连接:避免子表无数据时,主表字段被更新为 NULL;
- 语法检查:关键字别写错(
matched、update set缺一不可)。
2.不同数据库写法差异
- Oracle:原生支持 MERGE INTO,语法直接用;
- MySQL 8.0+:支持
MERGE INTO,低版本 MySQL 可用UPDATE JOIN替代; - SQL Server:语法和 Oracle 几乎一致,直接兼容。
3.额外优化建议
- 给关联字段加索引:
dept_id(部门表、员工表都要加); - 避免
like '%妖灵之地%'前置百分号,会导致索引失效,改成like '妖灵之地%'; - 大批量更新时,分批次执行,避免一次性锁表。
六、完结撒花!!
好了,如果你已经坚持阅读了这里,那么恭喜你!
你已经掌握了一个从“看似简单”的
UPDATE到“性能为王”的MERGE INTO的优化之路(快去和你小伙伴们装杯吧,哈哈哈!)
其实最后我还是想和大家说,不管我们做什么优化,抓住核心思路:能一次做完的事情,绝不做 N 次。这是我们SQL优化中的黄金法则!!!
所以,当你下次写UPDATE遇到子查询时,不妨多想一想:
- 这个子查询会被执行几次?—>如果答案是很多次,那本文实战的
MERGE INTO就是你最好的武器!!!
哈哈哈,如果感觉"有帮助",欢迎点赞,关注收藏哦!!!
半山腰很挤,道友~咱们顶峰见!!!
