福建省网站建设_网站建设公司_服务器部署_seo优化
2026/1/8 0:45:45 网站建设 项目流程

这篇文章用一个测试表完整跑通 Doris 的Partial Column Update(部分列更新)

  • 不存在就插入
  • 存在就只更新指定列
  • 值为NULL时不覆盖原值(保持原值)

1. 先搞懂:Partial Update 的前提条件

1.1 必须是 Unique Key 表,并且是 Merge-on-Write(MoW)

Doris 的Unique Key表天然支持 Upsert:同一个 Key 写入多次,只保留“最新结果”。

Partial Update(只更新部分列)INSERT来说,要求 Unique Key 表开启 Merge-on-Write

注意:如果你的旧表是 Merge-on-Read(MoR),很多情况下不能在线改成 MoW,会报类似 “Can not change UNIQUE KEY to Merge-On-Write mode”。
这种情况通常需要建新表迁移(后面会简单提一下思路)。


2. 创建一个测试表(支持 Partial Update)

我们创建一个简单测试表:用户画像/配置类数据很常见。

CREATETABLEtest_partial_upsert(uidBIGINT,name STRING,ageINT,city STRING,scoreINT,update_timeDATETIME)UNIQUEKEY(uid)DISTRIBUTEDBYHASH(uid)BUCKETS4PROPERTIES("enable_unique_key_merge_on_write"="true");

解释一下:

  • UNIQUE KEY(uid):以uid作为主键(唯一键)
  • enable_unique_key_merge_on_write=true:开启 MoW,后续才能用INSERT做 partial update

3. 开启 Partial Update:必须设置的两个变量

Doris 用INSERT做部分列更新,需要先设置会话变量:

SETenable_unique_key_partial_update=true;

官方文档说明:这个变量默认是false,不开就不允许用INSERT做部分列更新。

另外,为了实现“key 不存在就插入”,通常还要关闭 strict(严格)模式:

SETenable_insert_strict=false;

原因:严格模式下,导入/写入对不符合规则的数据会过滤或报错;在 partial update + 新 key 场景下,很多同学会碰到“被过滤/不生效”的情况,因此实践里常配合关闭 strict。

小提示:这些SET会话级别的,只对当前连接生效。生产环境里建议在应用拿到连接后统一设置(不要每条 SQL 都 SET)。


4. 不存在就插入,存在就更新(Upsert)

4.1 第一次写入:插入一整行

SETenable_unique_key_partial_update=true;SETenable_insert_strict=false;INSERTINTOtest_partial_upsert(uid,name,age,city,score,update_time)VALUES(1,'Alice',30,'Shanghai',80,'2026-01-07 10:00:00');

表里现在有:

  • uid=1, name=Alice, age=30, city=Shanghai, score=80

4.2 第二次写入:只更新部分列(Partial Update)

比如我只想更新scoreupdate_time,其它列保持不动:

SETenable_unique_key_partial_update=true;SETenable_insert_strict=false;INSERTINTOtest_partial_upsert(uid,score,update_time)VALUES(1,95,'2026-01-07 10:05:00');

结果:uid=1 那行会变成

  • name 仍然是 Alice(没动)
  • age、city 也不变
  • score 更新为 95

这就是Partial Update:只更新你写进列清单里的列。

4.3 如果 key 不存在:同样会插入新行

INSERTINTOtest_partial_upsert(uid,score,update_time)VALUES(2,60,'2026-01-07 10:06:00');

uid=2 不存在,就会插入一条新行(其它列为 NULL 或默认值,取决于表定义)。


5. 重点来了:如何做到「值为 NULL 就不更新原值」?

5.1 先说结论:Doris 目前不会自动忽略 NULL

在 Partial Update 模式下:

  • 只要某列出现在INSERT的列清单里
  • 且对应值是NULL
  • Doris 就会把目标列更新成NULL

也就是说,“NULL 不覆盖原值”不是默认行为。社区也有人提了增强需求,希望提供“忽略 NULL”的选项。

5.2 正确做法:NULL 的列不要出现在 INSERT 列清单里

例如你希望:

  • city 传 NULL 时不更新
  • score 有值才更新

写法是:当 city 为 NULL,就别把 city 写进 SQL

-- 只更新 score,不包含 cityINSERTINTOtest_partial_upsert(uid,score,update_time)VALUES(1,100,'2026-01-07 10:10:00');

这样city就会保持原值。

错误示例(会把 city 覆盖成 NULL):

INSERTINTOtest_partial_upsert(uid,city,score)VALUES(1,NULL,100);

6. 应用层怎么写:推荐两种方式

方式 A(最简单):按场景拆成多条 SQL

比如你有“更新分数”“更新城市”“更新年龄”几类场景,每个场景写固定列的INSERT

  • 需要更新的列写进去
  • 不需要的列不写进去
  • 永远不会把 NULL 覆盖到别的列

优点:最稳、最容易排查
缺点:SQL 会多一些

方式 B(更通用):MyBatis 动态 SQL(NULL 就不拼列)

这就是你们现在在做的方向。核心思想:
参数为 NULL → 不拼接该列,从而避免覆盖原值。

示例(通用 partial upsert mapper):

<insertid="upsertPartial"parameterType="com.demo.TestPojo">INSERT INTO test_partial_upsert (uid<iftest="name != null">, name</if><iftest="age != null">, age</if><iftest="city != null">, city</if><iftest="score != null">, score</if><iftest="updateTime != null">, update_time</if>) VALUES (#{uid}<iftest="name != null">, #{name}</if><iftest="age != null">, #{age}</if><iftest="city != null">, #{city}</if><iftest="score != null">, #{score}</if><iftest="updateTime != null">, #{updateTime}</if>)</insert>

这样:city=null时就不会写 city 列,自然也不会更新成 NULL。


7. 批量写入

你可能想把多条数据做成:

INSERTINTOtest_partial_upsert(uid,score)VALUES(1,10),(2,20),(3,30);

但动态 SQL 有一个天然矛盾:
批量 INSERT 要求每一行的列集合完全一致
如果 list 里有些对象city=null,有些对象city!=null,那列集合就不一致,不能一条 SQL 全塞进去。

正确做法按“列集合”分组,再分别批量 INSERT
思路:

  1. 给每个对象算一个 mask(哪些列非空)
  2. mask 相同的放一组
  3. 每组用一条固定列集合的批量 INSERT

1、mask 设计:用 bit 表示“本组要写哪些列”

我们固定 key 列必须写:uid
其它列:name/age/city/score/update_time可能为 null,希望 null 不更新 → 用 mask 控制它是否出现在 SQL 中。

publicfinalclassTestMask{publicstaticfinalintNAME=1<<0;publicstaticfinalintAGE=1<<1;publicstaticfinalintCITY=1<<2;publicstaticfinalintSCORE=1<<3;publicstaticfinalintUPDATE_TIME=1<<4;privateTestMask(){}}

计算 mask:

publicstaticintmask(TestPartialUpsertPojop){intm=0;if(p.getName()!=null)m|=TestMask.NAME;if(p.getAge()!=null)m|=TestMask.AGE;if(p.getCity()!=null)m|=TestMask.CITY;if(p.getScore()!=null)m|=TestMask.SCORE;if(p.getUpdateTime()!=null)m|=TestMask.UPDATE_TIME;returnm;}

解释:如果city=null,mask 不包含 CITY,这一批 SQL 就不会出现 city 列 →不会把数据库里的 city 改成 NULL


2、Java:分组 + 每组再按 500 分批写入

@ServicepublicclassTestPartialUpsertService{privatefinalTestPartialUpsertMappermapper;publicTestPartialUpsertService(TestPartialUpsertMappermapper){this.mapper=mapper;}@TransactionalpublicvoidupsertBatch(List<TestPartialUpsertPojo>list){if(list==null||list.isEmpty())return;// 1) 重要:同一连接里开启 session 变量(不要塞到 insert SQL 里)mapper.enablePartialUpdate();mapper.disableInsertStrict();// 2) 按 mask 分组Map<Integer,List<TestPartialUpsertPojo>>groups=list.stream().collect(Collectors.groupingBy(TestPartialUpsertService::mask));// 3) 每组做批量 INSERT(避免 SQL 太长,500 一批)for(Map.Entry<Integer,List<TestPartialUpsertPojo>>e:groups.entrySet()){intm=e.getKey();List<TestPartialUpsertPojo>g=e.getValue();for(inti=0;i<g.size();i+=500){List<TestPartialUpsertPojo>chunk=g.subList(i,Math.min(i+500,g.size()));mapper.upsertByMask(m,chunk);}}}privatestaticintmask(TestPartialUpsertPojop){intm=0;if(p.getName()!=null)m|=TestMask.NAME;if(p.getAge()!=null)m|=TestMask.AGE;if(p.getCity()!=null)m|=TestMask.CITY;if(p.getScore()!=null)m|=TestMask.SCORE;if(p.getUpdateTime()!=null)m|=TestMask.UPDATE_TIME;returnm;}}

3、MyBatis Mapper:三件事

3.1接口

@MapperpublicinterfaceTestPartialUpsertMapper{// session 变量:同一连接生效intenablePartialUpdate();intdisableInsertStrict();// 按 mask + 批量 upsertintupsertByMask(@Param("mask")intmask,@Param("list")List<TestPartialUpsertPojo>list);}

3.2 Pojo

publicclassTestPartialUpsertPojo{privateLonguid;privateStringname;privateIntegerage;privateStringcity;privateIntegerscore;privateLocalDateTimeupdateTime;// getter/setter 省略}

4、MyBatis XML:根据 mask 决定列集合(固定列集合 + foreach VALUES)

4.1 session SET(单独写,避免多语句 insert)

<updateid="enablePartialUpdate">SET enable_unique_key_partial_update = true</update><updateid="disableInsertStrict">SET enable_insert_strict = false</update>

4.2 核心:按 mask 生成“统一列集合”的批量 INSERT

<insertid="upsertByMask">INSERT INTO test_partial_upsert (uid<iftest="(mask & 1) != 0">, name</if><iftest="(mask & 2) != 0">, age</if><iftest="(mask & 4) != 0">, city</if><iftest="(mask & 8) != 0">, score</if><iftest="(mask & 16) != 0">, update_time</if>) VALUES<foreachcollection="list"item="x"separator=",">(#{x.uid}<iftest="(mask & 1) != 0">, #{x.name}</if><iftest="(mask & 2) != 0">, #{x.age}</if><iftest="(mask & 4) != 0">, #{x.city}</if><iftest="(mask & 8) != 0">, #{x.score}</if><iftest="(mask & 16) != 0">, #{x.updateTime}</if>)</foreach></insert>

为什么这个 XML 能保证“NULL 不更新原值”?

  • mask决定列是否出现
  • 例如 mask 没有 CITY,那么整条 SQL根本不包含city
  • Doris partial update 只会更新 SQL 中出现的列
  • 所以city保持原值

5、调用示例(验证效果)

List<TestPartialUpsertPojo>list=newArrayList<>();// uid=1:只更新 scorelist.add(newTestPartialUpsertPojo(1L,null,null,null,100,LocalDateTime.now()));// uid=2:更新 city + agelist.add(newTestPartialUpsertPojo(2L,null,28,"Shanghai",null,LocalDateTime.now()));// uid=3:更新 name(其余 null 不更新)list.add(newTestPartialUpsertPojo(3L,"Alice",null,null,null,null));service.upsertBatch(list);

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

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

立即咨询