生态实践

SQL 兼容性踩坑:从 PG/MySQL 应用迁至 KaiwuDB,哪些语法不兼容

原创Fang方2026-10-25
2

迁移最让人头疼的,往往不是报错,而是没报错。建表成功,批量导入结束,接口也返回 200;过几天才发现设备采样少了几行,或者重试写入后记录内容和原来不一样。这类问题常出在应用默认依赖的数据库行为上。设备采样要看时间戳精度和重复写入规则,业务表要看唯一键冲突怎么处理;当一笔业务跨到关系表和时序表时,还得重新核对事务边界。

一、先确定 SQL 落在哪种表上

假设一个设备平台同时保存设备档案和采样值。迁移 SQL 前,先把两类表分开看:设备档案通常是关系数据,采样值则进入时序表。表类型不同,应用能做的更新和删除也不同。

例如,关系表更新业务记录,与时序表修正设备数据,并不是可以互换的操作。时序表 UPDATE 用于更新标签值,不能修改主标签,条件也要按主标签等值筛选。时序 DELETE 按时间戳删除数据列记录;无条件删除时,数据列会被清理,标签保留。

比如设备注销时,旧逻辑可能对一张普通表执行 DELETE,并期待设备及其所有记录一起消失。迁到时序模型后,这个期待未必成立。逐表确认数据模型、定位字段、历史数据是否允许改写,以及注销后要保留哪些数据。若代码仍按普通关系表的方式更新历史采样、改主标签或清除设备数据,单纯改写 SQL 不够,还要调整数据模型或注销流程。

二、写入看似成功,时间和重复数据可能已经变了

采样数据经常用“设备 + 时间戳”定位。迁移时这两个字段看起来都在,数据仍可能对不上:目标精度变低,或者相同时间戳的写入互相覆盖。

KaiwuDB 时序时间戳支持毫秒、微秒、纳秒精度(3/6/9),默认精度为毫秒。若源库保存了微秒或纳秒时间,而目标列使用毫秒精度,不同时间可能被压到同一精度刻度。迁移前要对比源字段精度、目标列定义和导入格式;抽样检查时不能只看格式化后的时间字符串,也要比较实际时间差及同一设备的记录数。

重复时间戳还需单独验证。KaiwuDB 时序数据在相同时间戳下默认由后写入的数据覆盖已有数据,去重策略可以通过 ts.dedup.rule 配置。若源系统允许同设备同一时刻保留多条记录,或导入顺序不固定,“导入成功”并不能证明数据完整。至少对比迁移前后的行数、按设备和时间分桶的行数,并抽查重复时间戳样本及最终保留值。

档案表里的编号也要检查。KaiwuDB 的 SERIAL 是伪类型,对应 INT8 DEFAULT unique_rowid()。它不能只按名字等同于 MySQL AUTO_INCREMENT 或 PostgreSQL 序列。若应用把编号当作连续业务号、用它推断创建顺序,或会显式推进序列,先验证这些依赖;需要连续业务编号时,应单独设计编号策略。

三、冲突处理不是一回事:核对 upsert、唯一约束与删除

同步任务重跑时最容易碰到冲突。假设用户表以 id 为主键,同时 email 也要求唯一。应用想按邮箱更新用户资料,若迁移后只按主键处理冲突,第二次导入就可能报唯一约束错误,或留下与预期不同的行。

KaiwuDB 关系表 UPSERT 按主键冲突处理;非主键唯一约束发生冲突时可能报错。INSERT ... ON CONFLICT 可以指定冲突列,并使用 DO UPDATE 或 DO NOTHING。迁移时要核对源 SQL 的冲突目标:冲突由主键触发,还是由邮箱、设备编号等唯一键触发?应用要覆盖字段、忽略重复,还是让事务失败?不能把 REPLACE、ON DUPLICATE KEY UPDATE 或 ON CONFLICT 机械替换成 UPSERT。

用一组小数据做行为测试,比只比较语法更直接:先插入一条记录,再分别制造主键冲突和非主键唯一键冲突,记录每种写法最终保留的字段、返回值和错误。关系 INSERT 支持 RETURNING,应用若依赖插入后返回生成值,也要纳入测试。

下面这组关系表 SQL 可以放进隔离的测试库复现冲突路径。第二次写入使用相同邮箱、不同主键,冲突目标是非主键唯一列:

CREATE TABLE migration_user (
    id INT8 PRIMARY KEY,
    email STRING UNIQUE,
    display_name STRING
);

INSERT INTO migration_user VALUES (1, 'a@example.com', '旧名称');

-- 显式指定邮箱冲突时更新名称
INSERT INTO migration_user VALUES (2, 'a@example.com', '新名称')
ON CONFLICT (email) DO UPDATE
SET display_name = excluded.display_name;

SELECT id, email, display_name FROM migration_user;

检查点不是只看最后一条 SQL 是否成功,还要核对留下的 id 是否仍为 1、名称是否变为“新名称”,并对照应用原来在 PostgreSQL/MySQL 中的冲突目标和返回值。再将语句换成应用实际使用的 UPSERT,在同一测试表上记录结果。KWDB 的 UPSERT 按主键冲突处理,不会因为 email 是唯一列就自动按邮箱更新。

删除操作要区分“删除业务行”和“清理时序数据”。关系表的 DELETE 与 TRUNCATE 要检查事务、约束和应用期望;时序表还需验证标签保留行为。删除前后分别查询行数及关联信息,避免只看到 SQL 返回成功,就误以为设备或标签对象也已删除。

四、事务语句相似,事务边界仍要重画

从 PostgreSQL/MySQL 迁移事务代码,常见做法是保留 BEGIN、若干写操作和 COMMIT。KaiwuDB 3.2.2 支持 BEGIN / START TRANSACTION、COMMIT / END、ROLLBACK / ABORT 和保存点。不过,事务块本身不能证明整组跨模操作具备源库中相同的原子性和一致性。

默认隔离级别是 SERIALIZABLE,也可以使用 READ COMMITTED 或 REPEATABLE READ。隔离级别可在集群、会话或事务级配置,事务级设置优先级最高;集群级设置只对之后建立的连接生效。事务开始并发生 KV 读写后,不能再修改当前事务的隔离级别。使用连接池时要确认设置应用于哪些连接,避免只改了集群配置,却以为现有会话也已切换。

保存点可以局部回滚。事务出错后,可用 ROLLBACK TO SAVEPOINT 回到保存点继续执行。自动重试要看应用和驱动怎么发送事务:发生 40001 / retry transaction 时,KWDB 使用内部保存点 kwbase_restart。驱动整批发送语句时,重试可能覆盖整组操作;应用若只重跑失败的单条 SQL,结果就可能不同。测试时记录错误码和重试范围,再检查是否会重复扣款、重复发消息。

事务块不会自动把关系表和时序表变成一个原子单元。显式事务可以查询和写入时序数据,但不保证时序引擎的事务性,也不保证跨模查询结果一致;显式事务中不能执行时序 DDL。若订单写入关系表、设备状态写入时序表必须同时成功,不能只凭外层有 BEGIN 就认为失败时两边都会回滚。业务流程需要单独设计,再用故障注入检查补偿或重试策略。

可以先用纯关系表验证应用的保存点处理逻辑,再单独设计时序场景测试。以下示例预期最终将 balance 保持为 100:第一次更新发生在保存点之前,第二次更新回滚到保存点之后,随后提交事务。

CREATE TABLE migration_account (
    id INT8 PRIMARY KEY,
    balance DECIMAL
);

INSERT INTO migration_account VALUES (1, 100);

BEGIN;
UPDATE migration_account SET balance = 90 WHERE id = 1;
SAVEPOINT before_optional_step;
UPDATE migration_account SET balance = 0 WHERE id = 1;
ROLLBACK TO SAVEPOINT before_optional_step;
COMMIT;

SELECT balance FROM migration_account WHERE id = 1;

这个用例验证关系事务内的局部回滚,不证明时序写入也有相同保证。若生产代码通过驱动批量发送事务,再用同一业务逻辑验证驱动收到 40001 / retry transaction 时如何重试整组操作,并确认重试不会产生重复副作用。

另一个容易忽略的选项是 AS OF SYSTEM TIME:它只适用于只读事务;事务中包含写操作,或访问模式为 READ WRITE,系统会报错。若应用使用历史时间点查询,要把它与写事务分开。

五、类型、函数和标识符:不要只测“能不能建表”

DDL 脚本通过,并不代表数据映射正确。迁移验证还要覆盖类型边界、函数结果和对象命名。

类别 需要验证的行为 检查方式
时间类型 时序时间戳精度支持 3/6/9;时序表第一列声明为 TIMESTAMP 时按 TIMESTAMPTZ 处理;时区数据不存储,默认时区为 UTC 覆盖夏令时切换、时区转换、毫秒/微秒/纳秒边界和导出再导入
类型修改 已有时序时间戳列不能通过 ALTER COLUMN ... SET DATA TYPE 修改精度 将精度选择放在建表前评审,避免上线后直接改列
JSON JSON 是 JSONB 别名;重复对象键保留最后一个值 测试重复键、数值与字符串比较、应用序列化结果
字符与数组 CHAR/NCHAR 写入较短字符串不会补齐;数组是一维,不支持嵌套、索引或排序 检查定长填充、嵌套数组、数组排序或索引相关代码
函数 时序与关系数据有各自的函数和参数规则 对关键 SQL 表达式比较返回值、NULL 处理、时间边界和聚合结果
标识符/关键字 引用规则与关键字会影响对象名解析 扫描保留字、混合大小写、空格和特殊字符对象名,检查应用 SQL 与 ORM 生成语句

其中时间语义值得单独强调:时序表第一列声明为 TIMESTAMP 时按 TIMESTAMPTZ 处理,时区数据本身不存储,默认时区为 UTC。若应用把本地时间字符串当成绝对时间,或默认依赖会话时区转换,迁移后可能出现偏移。应明确输入时间是 UTC 还是本地时间,并用带时区与无时区样本验证驱动绑定和查询结果。

函数兼容性不能只看函数名。对关键 SQL 表达式,使用同一批边界输入分别在源库和目标库执行,比较值、类型、NULL、溢出和异常行为。时间函数、字符串大小写比较、JSON 操作和聚合函数的差异会直接影响业务结果。

六、迁移验收,盯住结果而不是执行状态

全量导入结束只是起点。把容易出问题的写入路径做成回归用例,比盯着导入进度条更有用:

场景 测试输入 对照结果
时间戳精度 同一设备在毫秒内多次采样 记录数、时间差、覆盖情况
重复数据 同一时间戳重复写入,调换导入顺序 最终值、保留策略、去重配置
更新/删除 更新标签、按时间戳删除、无条件删除 数据列和标签分别剩余什么
冲突处理 主键冲突与非主键唯一约束冲突 成功/报错、字段最终值、RETURNING 值
事务回滚 关系写入、时序写入、跨模读取分别失败 每种数据是否回滚,后续语句能否执行
并发与重试 制造事务冲突并触发客户端重试 错误码、重试范围、是否产生重复副作用
类型与函数 时间边界、JSON 重复键、字符串与数组边界 值、类型、NULL 与异常是否一致
标识符 混合大小写、关键字和特殊字符对象名 DDL、ORM 查询和迁移脚本是否一致

先挑出会影响余额、设备状态和告警结果的 SQL,连同失败路径一起跑。每条用例留下源库结果、KaiwuDB 3.2.2 结果和最终采用的处理方式。发现差异时,讨论的就是数据和行为,而不是“这条 SQL 到底算不算兼容”。这些脚本适合在隔离测试库运行;版本、驱动和查询结果也一并记录,后续改 SQL 或升级版本时可以重复验证。

结论

目前可以明确几件事:时序时间戳精度和重复写入策略会影响记录保留;SERIAL 使用 INT8 DEFAULT unique_rowid(),不能直接当作源库自增列;关系表 UPSERT 按主键冲突处理,可指定冲突列的 ON CONFLICT 是另一种处理方式;时序写入与跨模查询也不能直接套用关系事务的原子性预期。

所以,SQL 跑通只是迁移的第一步。真正需要对照的是数据留下了什么、冲突更新了哪一行,以及失败重试后有没有重复副作用。

-- 坚持原创,转载请注明出处 --

评论(0)

Me