上个月接手一个迁移评估对方递过来的材料是三样东西一份两千多行的建表脚本、一个导出的pg_dump全量 SQL 文件以及一句“应该问题不大吧都是关系型数据库”。我看完第一眼就回了句能跑通和跑得对是两码事。这两年从 PostgreSQL 迁到 GaussDB 的项目越来越多很多团队在前期评估阶段把工作量估得偏乐观真正动手才发现坑不在“建表”这一步而在函数、隐式转换、UPSERT、存储过程和执行计划这些地方。这篇就把我从几次 PG 到 GaussDB 迁移中攒下来的东西摊开讲——语法兼容性到底卡在哪几层、每一层怎么定位、怎么改写、怎么验证以及割接当天怎么保住退路。适合正在做迁移评估的 DBA、后端开发和架构同学也适合只是想把手里那套 PG 生态的 SQL 写得“更可移植”一点的人。1. 迁移前先把“兼容性”这三个字拆开看1.1 语法能跑通只是第一层语义一致才是真兼容很多人对“兼容”的理解停留在“这条 SQL 不报错”。这是最浅的一层。我把兼容性拆成四层从上往下成本依次翻倍层级判断标准典型表现排查手段语法层语句能被解析器接受关键字、函数名、子句顺序批量执行 DDL 脚本看报错类型层类型映射与隐式转换行为一致空串变 NULL、精度截断、比较结果不同逐表核对类型、抽样比对语义层同样输入得到同样输出排序规则、聚合空值处理、时区双跑比对结果集性能层执行计划与资源消耗可接受索引失效、算子退化、并行度差异EXPLAIN 对比、压测绝大多数迁移事故出在第二层和第三层。语法层反而是最好办的错一条改一条编译期就能暴露。类型层和语义层是“静默错误”——不报错但数据悄悄错了等业务方发现对账不平往往已经过了割接窗口。所以我在评估阶段的原则是宁可多花两天做双跑比对也不要在割接后半夜对着监控猜哪张表错了。提示凡是涉及金额、库存、积分、账单的记录迁移后必须做逐行或分片校验不能只信“行数对上了”。1.2 迁移成本到底花在哪一次真实的工时分布我复盘过三个中等规模项目的工时大致是这个分布DDL 改写和建库建表占 10% 到 15%SQL 与函数改写占 30%存储过程与触发器改造占 25%数据搬迁与校验占 20%剩下 10% 到 15% 是割接演练和回滚预案。注意存储过程那 25% 是波动最大的——如果原系统里有大量 PL/pgSQL 写的业务逻辑这块能直接翻倍。还有个容易被忽略的成本测试环境的等价性。PG 那边可能是 9.6 或 12目标库是某个具体版本的 GaussDB两者的优化器行为、可用的 hint、并行策略都不一样。如果测试环境的数据量只有生产的百分之一那你测出来的执行计划基本没有参考价值。我在一次项目里就吃过这个亏测试库上一切正常生产上一上量某个关联查询从走 Hash Join 变成了 Nested Loop响应时间从 80ms 涨到 4 秒。1.3 我用的兼容性盘点四步法第一步把源库的对象全量导出成清单。表、视图、物化视图、序列、索引、约束、函数、存储过程、触发器、自定义类型、扩展一个不漏。可以直接查系统表SELECT relname, relkind FROM pg_class WHERE relnamespace public::regnamespace AND relkind IN (r,v,m,S,i,c) ORDER BY relkind, relname;函数和过程单独拉一份SELECT n.nspname, p.proname, l.lanname, p.prokind FROM pg_proc p JOIN pg_namespace n ON n.oid p.pronamespace JOIN pg_language l ON l.oid p.prolang WHERE n.nspname NOT IN (pg_catalog,information_schema) ORDER BY l.lanname, p.proname;第二步按“改造量”给对象打标签。纯 DDL 的表打绿带默认值表达式、生成列、复杂约束的打黄带触发器、规则、自定义函数的打红。红的先做因为它的不确定性最高。第三步整理一份“特性使用清单”。重点盯这几个ON CONFLICT、RETURNING、WITH RECURSIVE、窗口函数、LATERAL、FILTER子句、GROUPING SETS、数组操作符、JSON 操作符、正则函数、generate_series、自定义聚合、扩展尤其是做模糊匹配和地理计算的。这份清单基本决定了你要改写多少条 SQL。第四步挑三到五条最核心的业务 SQL 做“双跑”。同一份数据源库跑一遍目标库跑一遍把结果集导出成文本做 diff。别小看这一步它能提前把 80% 的语义差异暴露出来。2. 类型系统与隐式转换九成的诡异报错都出在这2.1 基础类型的对应关系与几个反直觉的点大部分基础类型是对得上的int4、int8、numeric、varchar、text、timestamp、date、boolean都能直接映射。但有几个地方必须人工确认我列成表PG 侧写法目标库建议写法需要注意的点serial/bigserialserial或显式序列 默认值隐式创建的序列归属关系可能不同建议显式建序列texttext或varchar(n)目标库对超长字段的处理和存储方式有差异json/jsonbjson或目标库的 JSON 类型jsonb的操作符支持度要逐条验证uuid目标库 uuid 类型或varchar(36)部分版本对 uuid 的索引支持有限int[]数组谨慎使用数组类型的操作符和索引支持不完整能拆表就拆表moneynumeric(19,4)强烈建议不要用 money两边行为都不直观enum枚举类型或varchar 检查约束枚举值的增删改在两边成本不同serial这块我要多说一句。PG 里serial会自动建一个序列并设置OWNED BY删表时序列跟着删。目标库虽然也支持serial但我习惯显式写CREATE SEQUENCE seq_order_id START WITH 1 INCREMENT BY 1 NO CYCLE; CREATE TABLE t_order ( order_id BIGINT NOT NULL DEFAULT nextval(seq_order_id), ... ); ALTER SEQUENCE seq_order_id OWNED BY t_order.order_id;理由很简单显式写出来迁移工具解析的时候不会漏出问题也好查。隐式依赖越少搬迁越省心。2.2 空串与 NULL 的等价性差异这是最阴的一类坑能让人排查一整天。在标准 PG 行为里空字符串和NULL是两个不同的值 IS NULL返回 false。但在某些兼容 Oracle 语义的模式下空串会被当成 NULL 处理 IS NULL返回 true。这个差异会直接改变查询结果尤其是那些用WHERE col 做判断的老代码。我遇到过的一个具体场景订单备注字段允许空串业务代码里判断“有没有备注”用的是remark 。迁移之后这条判断永远查不到数据因为空串入库变成了 NULLNULL 的结果是 NULL 而不是 true。修复方式说简单也简单把判断改成remark IS NULL OR remark 但前提是你得先发现它。处理策略我一般这么定建库阶段确认目标实例的兼容模式把空串语义写进评估报告的“已知差异”里。应用层统一约定字符串字段不用空串表示“无值”一律用 NULL。数据搬迁时做一次清洗把历史空串统一转成 NULL并在应用侧同步调整判断逻辑。所有涉及字符串相等判断的索引检查是否受影响。注意如果目标库开启了某种兼容模式的行为可能和新建库时的模式设置绑定改模式不一定能改回来。这件事必须在建库之前定不要等到数据搬完了再纠结。2.3 隐式转换收严之后索引为什么突然不走了PG 在类型转换上相对宽松比如varchar和text比较、int和numeric比较通常能自动处理。目标库在某些场景下会收严或者转换规则不同结果就是同一条 SQL 不报错但索引失效了。举个典型的例子。假设user_id是bigint应用传参传成了字符串-- 源库上可能依然走索引 SELECT * FROM t_order WHERE user_id 2001;在某些实现里这种比较会先把列做隐式转换再比索引就用不上了。正确写法是显式转换参数SELECT * FROM t_order WHERE user_id CAST(2001 AS BIGINT);或者干脆在应用层保证参数类型和列类型一致。我个人的做法是迁移期间把所有核心 SQL 的EXPLAIN输出抓下来存档割接后逐条对比看有没有Seq Scan替换掉Index Scan的情况。这个对比工作量不小但比事后救火便宜得多。另外一个高频点是LIKE和||拼接后的类型推导。WHERE name LIKE % || ? || %这种写法在参数类型不明确时两边可能推出不同的结果类型。稳妥的写法是显式CAST(? AS TEXT)。3. DDL 层的高频不兼容点逐个拆3.1 建表语句自增列、默认值、注释、存储属性建表语句看着最人畜无害其实细节最多。我按踩坑频率排了个序自增列。除了前面说的serial显式化还要注意GENERATED ALWAYS AS IDENTITY的支持情况。老一点的版本可能不认这个语法那就退回序列方案。另外注意OVERRIDING SYSTEM VALUE在数据搬迁时的作用——如果你要往 identity 列里灌历史数据不加这个子句会报错。默认值表达式。now()、current_timestamp、CURRENT_DATE一般都支持。但gen_random_uuid()这类函数依赖扩展目标库未必有。替代方案是在应用层生成 UUID或者用目标库自带的函数。还有一些用nextval之外的自定义函数做默认值的需要逐个确认函数已迁移。注释。COMMENT ON TABLE、COMMENT ON COLUMN一般没问题但中文注释要注意字符集。建库时如果字符集选的不是 UTF8中文注释会变成乱码。这一点在建库脚本的第一行就要定死。存储属性。PG 的WITH (fillfactor70)、TABLESPACE、UNLOGGED这些子句目标库的支持度各不相同。fillfactor通常支持表空间要看部署方式UNLOGGED在很多实现里不支持需要删掉。这类子句不影响逻辑正确性但会影响脚本能否顺利执行建议在批量执行前先做一次“语法预检”——把所有 DDL 丢进一个空库跑一遍看报错清单。# 预检思路逐条执行记录失败语句不中断 while IFS read -r line; do echo $line | psql -h target -d testdb -v ON_ERROR_STOP0 2 ddl_errors.log done schema.sql实际用的时候建议按分号切分语句别按行切否则多行语句会被切碎。3.2 分区表的语法迁移分区是 DDL 里改造量最大的一块。PG 10 之后用的是声明式分区PARTITION BY RANGE (col)子分区用FOR VALUES FROM ... TO ...目标库的分区语法更接近早期的写法子分区用VALUES LESS THAN (...)。这两套语法不通用必须重写。-- 源库写法 CREATE TABLE t_log ( id BIGINT, log_date DATE, content TEXT ) PARTITION BY RANGE (log_date); CREATE TABLE t_log_202401 PARTITION OF t_log FOR VALUES FROM (2024-01-01) TO (2024-02-01);-- 目标库写法 CREATE TABLE t_log ( id BIGINT, log_date DATE, content TEXT ) PARTITION BY RANGE (log_date); CREATE TABLE t_log_202401 PARTITION OF t_log FOR VALUES FROM (2024-01-01) TO (2024-02-01);上面这个只是理想情况实际差异往往在是否支持DEFAULT分区、是否支持哈希分区、分区键能否是多列、子分区能否再分区、分区索引是全局还是本地。我建议的做法是先确认目标版本的分区能力清单再决定分区策略。如果原系统用的是哈希分区而目标库不支持就得改成按时间或按取模列做范围分区这会牵动应用层的路由逻辑。还有一个隐形成本分区表的数量。有的系统按月分区三年就是 36 个分区加上索引就是上百个对象。手工改写不现实一定要写脚本批量生成。# 批量生成分区 DDL 的简化示例 from datetime import date, timedelta def month_range(start: date, months: int): cur start for _ in range(months): nxt date(cur.year (cur.month // 12), cur.month % 12 1, 1) yield cur, nxt cur nxt for s, e in month_range(date(2024, 1, 1), 12): name ft_log_{s.strftime(%Y%m)} print(fCREATE TABLE {name} PARTITION OF t_log fFOR VALUES FROM ({s}) TO ({e});)3.3 索引与约束的落地差异索引这块B-Tree、唯一索引、复合索引基本都能平移。需要重点验证的是表达式索引、部分索引带WHERE的索引、GIN/GiST索引、BRIN索引。部分索引在两边都支持但谓词表达式的写法可能要调整。表达式索引要求函数是 immutable 的如果原索引用了自定义函数得确认目标库上这个函数存在且被标记为 immutable。GIN索引常用于数组和 JSON 字段迁移前要确认目标库的 opclass 是否齐全。BRIN索引是 PG 的特色很多目标库不提供遇到大表的 BRIN 索引替代方案是改成分区表加本地索引。约束方面外键和唯一约束一般没问题麻烦的是CHECK约束里的表达式。比如CHECK (amount 0 AND status IN (a,b))字符串列表和布尔运算通常没问题但如果里面用了正则或者自定义函数就要单独验证。还有一个细节DEFERRABLE约束的支持度。PG 允许把唯一约束和外键声明为可延迟在某些批量导入场景下很有用目标库不一定支持不支持就得改导入顺序。4. DML 与查询层的改写实战4.1 UPSERT从 ON CONFLICT 到 MERGE INTO这是改写量最大的一类语句。PG 的INSERT ... ON CONFLICT ... DO UPDATE写法简洁但目标库大多不直接支持这个语法标准替代是MERGE INTO。-- 源库ON CONFLICT INSERT INTO t_stock (sku_id, qty, updated_at) VALUES (1001, 20, now()) ON CONFLICT (sku_id) DO UPDATE SET qty t_stock.qty EXCLUDED.qty, updated_at EXCLUDED.updated_at;-- 目标库MERGE INTO MERGE INTO t_stock t USING (SELECT 1001 AS sku_id, 20 AS qty, now() AS updated_at) s ON (t.sku_id s.sku_id) WHEN MATCHED THEN UPDATE SET qty t.qty s.qty, updated_at s.updated_at WHEN NOT MATCHED THEN INSERT (sku_id, qty, updated_at) VALUES (s.sku_id, s.qty, s.updated_at);改写的时候有几个坑要盯住第一EXCLUDED是 PG 的特有引用方式在MERGE里要换成USING子句的别名。别直接照抄否则语义会错。第二MERGE的并发行为需要实测。两个会话同时MERGE同一条不存在的记录可能出现唯一键冲突或者双写。PG 的ON CONFLICT在这一点上语义更明确。我的处理办法是在应用层加分布式锁或者把MERGE包在重试逻辑里捕获唯一键冲突后重试一次。第三批量MERGE的性能。源库可能是一次INSERT带几百行VALUES换成MERGE后每行一个USING子查询性能会掉。改进方式是用UNNEST或者临时表MERGE INTO t_stock t USING (SELECT * FROM tmp_stock_batch) s ON (t.sku_id s.sku_id) WHEN MATCHED THEN UPDATE SET qty t.qty s.qty WHEN NOT MATCHED THEN INSERT (sku_id, qty) VALUES (s.sku_id, s.qty);先把批量数据灌进临时表再一次性MERGE这个模式在两边都能跑性能也稳定。我现在的习惯是凡是批量写入一律走临时表 MERGE不再用多值VALUES。4.2 RETURNING、LIMIT/OFFSET 与递归 CTERETURNING在 PG 里用得极多插入后直接拿回自增 ID。目标库的支持情况分版本有的支持INSERT ... RETURNING有的只支持部分场景。如果目标库不支持替代方案有三个插入前先SELECT nextval(seq)拿 ID再显式插入。缺点是序列会被消耗但可能没插入成功出现空洞。空洞本身不是问题但如果业务对 ID 连续性有要求就麻烦了。插入后用唯一业务键回查。多一次查询但在唯一索引下成本可接受。用MERGE加上RETURNING如果支持或者用存储过程封装。我一般选方案 2因为不依赖序列行为并发下也安全。LIMIT/OFFSET两边都支持深分页的性能问题也一样。OFFSET 100000这种写法在哪都慢迁移是改造它的好时机换成基于游标的分页-- 改造前 SELECT * FROM t_order ORDER BY order_id LIMIT 20 OFFSET 100000; -- 改造后记住上一页最后一个 order_id SELECT * FROM t_order WHERE order_id 100000 ORDER BY order_id LIMIT 20;递归 CTEWITH RECURSIVE一般都能用但要注意递归终止条件的写法差异。有的实现对递归深度的默认限制不同遇到树形结构查询时要显式设置上限否则可能直接报错或者查询超时。4.3 函数、正则与 JSON 的替代写法字符串函数大部分通用substr、trim、upper、lower、replace、position、concat、string_agg都能找到对应。需要留意的是split_part、regexp_split_to_table、string_to_array这类支持度不一。正则是最容易出问题的。PG 用~、~*、!~做匹配用regexp_match、regexp_matches、regexp_replace做提取替换。目标库通常支持~系列和regexp_replace但regexp_matches返回多行的行为可能不同。稳妥的做法是把正则逻辑挪到应用层或者改用substrposition的组合。JSON 这块要格外注意。PG 的jsonb有丰富的操作符-、-、#、#、、?、?|、?。目标库如果只有json类型这类包含操作符可能不支持?系列也可能缺失。改写的思路是把常用的 JSON 路径提取改成-逐层取把包含查询改成把关键字段抽出来建独立列。-- 源库jsonb 包含查询 SELECT * FROM t_event WHERE payload {type:pay}; -- 目标库改成路径提取 普通比较 SELECT * FROM t_event WHERE payload - type pay;后一种写法在数据量大时性能差因为没有索引。真要在 JSON 上做高频查询正确做法是加一个生成列或者冗余列在上面建索引。这个改造要在迁移评估阶段就提出来别等上线后才发现慢。5. 存储过程与触发器改造量最大的一块5.1 过程语言与语法糖的差异两边都支持 PL/pgSQL 风格的存储过程基础语法大体一致DECLARE、BEGIN、EXCEPTION、END、IF、LOOP、FOR、RAISE。但细节差异不少特性PG 侧目标库侧处理建议参数默认值支持支持度不一不支持的改成重载多个过程RETURNS TABLE支持一般支持注意列名冲突SETOF返回支持支持度不一改用RETURNS TABLE包PACKAGE不支持部分版本支持不要依赖用命名前缀替代自治事务不支持部分版本支持不要在核心链路依赖动态 SQLEXECUTE ... USING支持语法略有差异逐条验证游标FOR rec IN cursor支持注意NO SCROLL选项最麻烦的是参数默认值和重载。PG 允许func(a int, b int default 0)调用时只传a。如果目标库不支持默认值就得拆成两个过程func(a)和func(a, b)。这个改造量取决于你原来写了多少带默认参数的过程我见过一个系统里有两百多个纯手改不现实只能写脚本辅助。5.2 三个高频改写模板模板一分页查询过程。原来用LIMIT/OFFSET加动态拼接改写成显式参数加游标。CREATE OR REPLACE PROCEDURE sp_query_order( p_user_id BIGINT, p_offset INT, p_limit INT ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT order_id, amount, created_at FROM t_order WHERE user_id p_user_id ORDER BY created_at DESC LIMIT p_limit OFFSET p_offset; END; $$;如果目标库对RETURN QUERY支持不好改成RETURNS TABLE加RETURN NEXT循环。模板二批量更新带异常捕获。这类过程在两边都要注意事务边界。CREATE OR REPLACE PROCEDURE sp_batch_update() LANGUAGE plpgsql AS $$ DECLARE v_id BIGINT; BEGIN FOR v_id IN SELECT order_id FROM t_order WHERE status pending LOOP BEGIN UPDATE t_order SET status done WHERE order_id v_id; EXCEPTION WHEN OTHERS THEN RAISE NOTICE order % failed: %, v_id, SQLERRM; END; END LOOP; END; $$;注意内层的BEGIN ... EXCEPTION会形成一个子事务在某些实现里开销不小。批量场景下建议改成集合操作实在要逐行处理把异常处理挪到外层。模板三返回结果集的过程。用RETURNS TABLE比SETOF record更稳CREATE OR REPLACE FUNCTION fn_order_stat(p_date DATE) RETURNS TABLE (status TEXT, cnt BIGINT, total NUMERIC) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT o.status, COUNT(*), SUM(o.amount) FROM t_order o WHERE o.created_at::date p_date GROUP BY o.status; END; $$;::date这种转换在两边的行为一致性要验证某些实现里对timestamp转date的截断规则和时区处理不同。5.3 异常处理与事务控制的注意点PL/pgSQL 的EXCEPTION块在两边语义基本一致但有几个地方要留心。第一SQLSTATE码的覆盖范围。PG 的错误码定义比较完整目标库可能只实现了常用的一部分。如果你在过程里靠WHEN unique_violation判断冲突这个能覆盖到但如果是靠具体数字码判断就要逐个核对。第二EXCEPTION块内部回滚的范围。PG 里子事务回滚会撤销块内的所有操作包括序列的nextval序列不回滚这是常识。目标库行为要实测。第三过程内不能直接COMMIT或ROLLBACK除非支持自治事务。原来在过程里做分段提交的逻辑必须挪到应用层用多个事务调用来实现。这个改造会影响失败重试的语义要在设计阶段想清楚。6. 数据迁移与割接的完整实操流程6.1 全量 增量的搬数策略数据量在几十 GB 以内、停机窗口一小时以上的直接逻辑导出加导入最省事。用pg_dump导出结构再按表导出数据pg_dump -h source -U app -d proddb --schema-only -f schema_pg.sql pg_dump -h source -U app -d proddb --data-only --tablet_order \ -f t_order_data.sql结构文件不能直接灌进目标库要先过一遍改写脚本。数据文件如果只是COPY ... FROM stdin格式通常可以直接用但要注意转义字符和 NULL 标记的处理差异。数据量大或者窗口短的必须走增量同步。思路是先做一次全量快照记录快照位点然后持续订阅增量变更追平后短暂停写做最后一次增量同步和校验再切换。这里的关键是增量变更的捕获方式——逻辑复制、触发器记录变更表、或者解析 WAL。不同方式的侵入性和可靠性差别很大触发器方式会影响源库写入性能逻辑复制方式对源库版本有要求。我的经验是变更频率高的表走增量几乎不改的历史表走全量日志类表可以只搬最近几个月。整体搬迁前先按表分级能省掉大量时间。6.2 一致性校验怎么做才靠谱行数校验只是第一道关必须做二级校验。我一般用三层层级方法成本能发现的问题第一层COUNT(*)比对低漏表、漏行第二层按主键范围分组做SUM/MD5聚合比对中字段值错误、类型截断第三层抽样逐行 diff高编码、排序、精度问题第二层的实现方式按主键区间切分比如每 10 万条算一次聚合值两边对比。SELECT (order_id / 100000) AS bucket, COUNT(*) AS cnt, SUM(amount) AS sum_amount, SUM(LENGTH(COALESCE(remark, ))) AS remark_len FROM t_order GROUP BY bucket ORDER BY bucket;把这份结果导出成 CSV两边做 diff。对不上的桶再进去做逐行比对。这个办法比全表MD5实用得多因为MD5对全表拼接一次内存扛不住而分桶后可以并行。字符串字段的比对要特别注意编码不同会导致看起来一样的字符算出不同的哈希。校验前先统一字符集或者把字符串转成bytea再算。6.3 割接窗口与回滚预案割接我按时间轴排每一步都有明确的完成标志和失败动作T-7 天完成全量迁移和首轮校验差异清零。T-1 天停写演练一次测出真实的最后一次增量同步耗时和校验耗时。T0停止应用写入等待在途事务完成通常 30 秒到 2 分钟。T02min做最后一次增量同步直到无新变更。T05min跑第二层校验差异为 0 则继续。T08min切换应用连接串放开灰度流量。T015min观察核心指标错误率、慢查询数、连接数。失败动作任一环节超时或校验不过立即回切源库源库保留可写状态至少 24 小时。这里有两个容易忽略的点。一是源库在割接后不要立刻停掉至少保持只读或者可写一到两天给回滚留余地。二是应用连接串的切换要能秒级生效配置中心或者 DNS 都行别让运维手工改配置文件重启服务。我见过一次割接因为配置没推送下去一半实例连新库一半连旧库数据瞬间双写后果很难收拾。7. 常见问题速查表与几条压箱底的经验7.1 问题速查表现象可能原因排查动作语句报语法错误位置在ON CONFLICTUPSERT 语法不支持改MERGE INTO或ON DUPLICATE KEY UPDATE查询结果少了记录空串被当成 NULL检查 判断改成IS NULL OR 明明有索引却走全表扫描隐式类型转换、统计信息缺失EXPLAIN对比显式CAST收集统计信息插入后拿不到自增 IDRETURNING不支持改nextval预取或唯一键回查中文显示为问号字符集不是 UTF8建库时指定导入前转码存储过程编译不过默认参数、SETOF返回拆重载、改RETURNS TABLE分区表建不上分区语法或分区类型不支持核对版本能力改范围分区批量导入特别慢索引和约束在导入时维护先删索引导入后重建迁移后慢查询暴涨优化器差异、统计信息未收集收集统计信息必要时加 hint权限不足无法建表权限模型三权分立开启用初始用户或调整角色配置7.2 我自己踩过的几个坑第一个坑统计信息没收集就压测。数据刚灌进去优化器手里没有统计信息执行计划全是猜的。这时候压测出来的性能数据毫无意义甚至可能让你误判某个索引没用。正确顺序是灌数据、ANALYZE、再压测。这个大坑我在第一个项目里踩得很结实。第二个坑ORDER BY结果不一致。两边排序规则或者默认排序稳定性不同同一条带ORDER BY created_at的查询在时间戳相同时返回的顺序可能不同导致分页丢数据或者重复数据。解决办法是排序键后面追加主键保证唯一性ORDER BY created_at DESC, order_id DESC。这是个小改动但能省掉很多“偶发丢数据”的排查。第三个坑时区。源库用timestamp without time zone存本地时间应用层自己做时区转换目标库所在机器时区设置不同now()返回的值就不一样。迁移前必须确认列类型是带时区还是不带时区、服务器的timezone参数、应用连接串里的时区设置。三者要统一。第四个坑扩展缺失。原系统用某个扩展做模糊匹配或者相似度计算目标库没有对应扩展。这类问题没有优雅的替代方案要么把计算挪到应用层要么用LIKE加前缀索引凑合。评估阶段一定要把扩展清单单独列出来逐个确认。第五个坑迁移工具“自作聪明”。有的工具在转换类型时会自动把text改成varchar(255)理由是“更规范”。结果超长内容被截断而且不报错。凡是自动转换的产物都要人工过一遍类型定义的 diff。最后分享一个我觉得最值钱的做法建一个兼容性测试用例库。把你踩过的每个坑写成一条最小可复现的 SQL加上期望结果。迁移前跑一遍割接后回归一遍后面再有新项目直接复用。这个库前三次维护成本高之后就变成资产了。我现在的用例库大概两百多条覆盖了类型转换、聚合、窗口函数、正则、JSON、过程语法几大类新项目评估基本半天就能出结论。
阅读完成 · 觉得有帮助?