MySQL 切 PostgreSQL,不只是改个驱动
原项目技术栈是:
Spring Boot + MyBatis-Plus + MySQL
最开始切 PostgreSQL 时,以为只是:
换一个数据库驱动;
改一下 JDBC URL;
调整一下数据库连接配置。
实际迁移过程中才发现,真正麻烦的不是连接数据库,而是 SQL 方言、字段类型、事务行为和隐式转换规则的差异。
核心结论是:
MySQL 是相对宽容的数据库方言,PostgreSQL 更严格。
从 MySQL 迁移到 PostgreSQL,最大的坑不是 SQL 写法,而是 MySQL 中大量“自动隐式转换”的习惯在 PostgreSQL 中失效。
整理几个迁移过程中常见的问题。
一、基础切换流程
1. 引入 PostgreSQL 驱动
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
2. 修改 JDBC 连接信息
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://数据库地址/数据库名?currentSchema=模式名
PostgreSQL 驱动只识别 PostgreSQL 自己的参数。
MySQL 中常见的这些参数不要直接带过来:
useUnicode
characterEncoding
serverTimezone
useSSL
PostgreSQL 可以按需配置自己的参数,例如:
ApplicationName=myApp
connectTimeout=10
socketTimeout=30
ssl=true
sslmode=require
如果要开启 SSL,也需要使用 PostgreSQL 自己的参数:
ssl=true&sslmode=require
3. 注意 schema 概念
PostgreSQL 比 MySQL 多了一层 schema 概念。
一个 PostgreSQL 数据库下可以有多个 schema。
JDBC URL 中的:
currentSchema=模式名
可以理解为指定当前连接默认访问的 schema。
如果不指定,默认使用:
public
二、大坑一:类型转换异常
这是迁移过程中最容易遇到的问题之一。
MySQL 对类型转换比较宽松,比如字段是 tinyint,传入 boolean,很多情况下也能自动处理。
PostgreSQL 则更严格,字段类型和参数类型不匹配时,会直接报错。
1. 查询时类型不匹配
例如字段 is_active 是 smallint,但查询时传入了 true:
SELECT *
FROM orders
WHERE is_active = true;
PostgreSQL 可能会报错:
operator does not exist: smallint = boolean
2. 更新时类型不匹配
例如字段 is_premium 是 smallint,但更新时传入了 false:
UPDATE orders
SET is_premium = false
WHERE is_premium = true;
可能会报错:
column "is_premium" is of type smallint but expression is of type boolean
3. 解决方式
有两类处理方式。
第一种是从字段类型或代码类型上对齐。
例如:
PostgreSQL 字段改成
boolean;或者 Java 代码中的字段类型改成
Integer;总之保证数据库字段类型和代码传参类型一致。
这是优先方案。
第二种是在 PostgreSQL 中注册隐式类型转换函数。
例如 smallint 转 boolean:
CREATE OR REPLACE FUNCTION smallint_to_boolean(i int2)
RETURNS bool AS $BODY$
BEGIN
RETURN (i::int2)::integer::bool;
END;
$BODY$ LANGUAGE plpgsql VOLATILE;
CREATE CAST (smallint AS boolean)
WITH FUNCTION smallint_to_boolean
AS ASSIGNMENT;
再注册 boolean 转 smallint:
CREATE OR REPLACE FUNCTION boolean_to_smallint(b bool)
RETURNS int2 AS $BODY$
BEGIN
RETURN (b::boolean)::bool::int;
END;
$BODY$ LANGUAGE plpgsql VOLATILE;
CREATE CAST (boolean AS smallint)
WITH FUNCTION boolean_to_smallint
AS IMPLICIT;
但这种方式需要谨慎使用。
隐式转换注册过多,可能导致 PostgreSQL 在解析操作符时找不到唯一的最优匹配,从而出现类似错误:
Could not choose a best candidate operator
operator is not unique
所以能改代码和字段类型时,优先改代码和字段类型。
三、大坑二:TIMESTAMPTZ 与 LocalDateTime 不兼容
迁移后可能遇到类似错误:
PSQLException: Cannot convert the column of type TIMESTAMPTZ to requested type java.time.LocalDateTime.
这是因为 PostgreSQL 的 TIMESTAMPTZ 是带时区的时间戳,而 Java 中的 LocalDateTime 不带时区。
两者不能直接对应。
解决方式
可以选择以下方案:
PostgreSQL 表字段改成
timestamp,也就是不带时区的时间戳;或者 Java 字段类型改成
OffsetDateTime/Date。
如果项目中 Java 时间字段大量使用 LocalDateTime,建表时建议直接使用:
timestamp
避免迁移工具默认生成:
timestamptz
否则后续需要修改大量字段或 Java 类型。
四、大坑三:事务异常会污染整个事务
PostgreSQL 中,如果同一个事务里某条 SQL 执行失败,整个事务会进入异常状态。
之后即使代码捕获了异常,继续执行其他 SQL,也可能报错:
ERROR: current transaction is aborted, commands ignored until end of transaction block
这个行为和很多 MySQL 项目的使用习惯不同。
典型问题代码
例如下面这种逻辑:
if (assigneeStatistics == null) {
StatisticsEntity entity = new StatisticsEntity();
entity.setProcessDefKey(processDefKey);
entity.setTaskDefKey(taskDefKey);
entity.setUserId(userId);
entity.setChannel(channel);
entity.setQuantity(1);
try {
save(entity);
} catch (DuplicateKeyException e) {
statisticsMapper.increaseByUnique(
processDefKey,
taskDefKey,
userId,
channel
);
}
} else {
statisticsMapper.increase(assigneeStatistics.getId());
}
代码意图是:
先尝试插入;
如果唯一键冲突;
捕获异常;
再执行更新。
在 MySQL 中,这类写法可能还能继续执行。
但在 PostgreSQL 中,save(entity) 一旦抛出数据库异常,当前事务就已经进入异常状态。
此时再执行:
statisticsMapper.increaseByUnique(...);
就可能继续失败。
解决思路
不要用数据库异常控制业务流程。
更稳妥的做法是先判断记录是否存在,再决定执行 INSERT 还是 UPDATE。
也就是说,把:
先 INSERT,失败后 catch,再 UPDATE
改成:
先 SELECT,判断存在后再 INSERT 或 UPDATE
迁移 PostgreSQL 时,项目中所有类似下面的代码都需要重点排查:
catch SQLException / DuplicateKeyException 后继续执行数据库操作
这类代码在 PostgreSQL 中很容易触发事务污染问题。
五、中坑一:JSON 字段语法不同
MySQL 和 PostgreSQL 的 JSON 查询语法不一样。
MySQL 中常见写法:
WHERE order_meta->'$.coupon_code' LIKE CONCAT('%', ?, '%')
PostgreSQL 中一般写成:
WHERE order_meta ->> 'coupon_code' LIKE CONCAT('%', ?, '%')
差异点是:
如果项目里 JSON 字段使用较多,迁移时需要集中排查这类 SQL。
六、中坑二:date_format 不存在
MySQL 中常用:
DATE_FORMAT(create_time, '%Y-%m-%d')
PostgreSQL 中没有 DATE_FORMAT,需要改成:
to_char(create_time, 'YYYY-MM-DD')
常见替换关系如下:
to_char(create_time, 'YYYY-MM-DD')
对应 MySQL:
DATE_FORMAT(create_time, '%Y-%m-%d')
to_char(create_time, 'YYYY-MM')
对应 MySQL:
DATE_FORMAT(create_time, '%Y-%m')
to_char(create_time, 'YYYYMMDDHH24MISS')
对应 MySQL:
DATE_FORMAT(create_time, '%Y%m%d%H%i%s')
七、中坑三:GROUP BY 更严格
PostgreSQL 对 GROUP BY 的要求更严格。
如果 SELECT 中出现的字段既不在 GROUP BY 中,也没有被聚合函数包裹,就会报错。
典型错误:
ERROR: column "u.user_name" must appear in the GROUP BY clause or be used in an aggregate function
错误示例
SELECT user_name, age, COUNT(*)
FROM user_profile
GROUP BY age, score;
这里 user_name 出现在 SELECT 中,但没有出现在 GROUP BY 中,也没有被聚合函数包裹。
PostgreSQL 会直接报错。
修改方式一:加入 GROUP BY
SELECT user_name, age, COUNT(*)
FROM user_profile
GROUP BY user_name, age, score;
修改方式二:使用聚合函数
SELECT MIN(user_name), age, COUNT(*)
FROM user_profile
GROUP BY age, score;
MySQL 项目中如果 GROUP BY 写得比较宽松,迁移到 PostgreSQL 时,需要全文检索 GROUP BY 并逐一检查。
八、小坑速查
下面这些属于常见语法差异,遇到后按规则修改即可。
这些问题修改成本通常不高,适合通过 IDE 全文搜索逐步处理。
九、迁移辅助脚本
1. 批量把 timestamptz 改成 timestamp
说明:
timestamp without time zone = timestamp
timestamp with time zone = timestamptz
脚本示例:
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = '要处理的模式名'
AND data_type = 'timestamp with time zone'
LOOP
EXECUTE 'ALTER TABLE '
|| rec.table_name
|| ' ALTER COLUMN '
|| rec.column_name
|| ' TYPE timestamp';
END LOOP;
END $$;
2. 批量给 create_time / update_time 设置默认值
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = '要处理的模式名'
AND data_type = 'timestamp without time zone'
AND column_name IN ('create_time', 'update_time')
LOOP
EXECUTE 'ALTER TABLE '
|| rec.table_name
|| ' ALTER COLUMN '
|| rec.column_name
|| ' SET DEFAULT CURRENT_TIMESTAMP;';
END LOOP;
END $$;
注意字符串拼接时要保留必要的空格,否则拼出来的 SQL 可能不合法。
十、迁移注意事项
1. 字段类型要严格对应
迁移数据表时,不要想当然修改字段类型。
尤其是 MySQL 中的:
tinyint
迁移到 PostgreSQL 后,需要确认它在代码中到底对应的是:
Integer / Short
还是:
Boolean
字段类型和 Java 类型不一致时,很容易触发 PostgreSQL 类型转换异常。
2. LocalDateTime 不要直接对应 TIMESTAMPTZ
如果 Java 中使用:
LocalDateTime
PostgreSQL 字段建议使用:
timestamp
不要直接使用:
timestamptz
否则可能出现类型转换问题。
3. 不要依赖 MySQL 的隐式转换
MySQL 中很多可以自动转换的写法,到了 PostgreSQL 中会直接失败。
例如:
tinyint ↔ boolean
在 MySQL 中可能可以自动处理。
但 PostgreSQL 中需要保证字段类型和参数类型严格一致。
4. 排查 catch 后继续操作数据库的代码
PostgreSQL 中,一条 SQL 在事务中出错后,整个事务可能进入异常状态。
因此需要重点排查:
catch 数据库异常后,继续执行数据库操作
这类代码在 MySQL 中可能能跑,在 PostgreSQL 中可能会失败。
结论
从 MySQL 切 PostgreSQL,不只是改一个驱动包,也不只是改 JDBC URL。
真正需要关注的是这些差异:
PostgreSQL 类型检查更严格;
MySQL 中常见的隐式转换,在 PostgreSQL 中可能失效;
TIMESTAMPTZ和 JavaLocalDateTime不能直接对应;PostgreSQL 中事务一旦异常,后续 SQL 可能无法继续执行;
JSON、日期格式化、
GROUP BY、字符串和字段引用方式都有语法差异;迁移前需要逐一排查 SQL、字段类型和事务异常处理逻辑。