从 MySQL 到 PostgreSQL
最后一章,把全书对 MySQL 的对照汇成一张能直接抄的速查表——语法、类型、UPSERT、大小写与那些坑。带着你多年的 MySQL 直觉,稳稳落地 PostgreSQL。然后,我们用「当真」这两个字,给这趟旅程收个尾。
语法速查:写法上的差异
| 你在 MySQL 里写 | 在 PostgreSQL 里写 |
|---|---|
`column`(反引号) | "column"(双引号,且区分大小写) |
id INT AUTO_INCREMENT | id integer GENERATED ALWAYS AS IDENTITY |
LIMIT 10, 20(偏移,行数) | LIMIT 20 OFFSET 10 |
CONCAT(a, b) | a || b(或也支持 concat) |
IFNULL(a, b) | COALESCE(a, b) |
NOW(), CURDATE() | now(), current_date |
col LIKE '%x%'(默认不分大小写) | col ILIKE '%x%'(PG 的 LIKE 区分大小写) |
INSERT ... ON DUPLICATE KEY UPDATE | INSERT ... ON CONFLICT (key) DO UPDATE |
REPLACE INTO | INSERT ... ON CONFLICT ... DO UPDATE |
GROUP_CONCAT(x) | string_agg(x, ',') |
DESCRIBE t / SHOW TABLES | \d t / \dt(psql 元命令) |
SHOW CREATE TABLE t | \d t,或 pg_dump -t t --schema-only |
字符串用 "..." 或 '...' | 字符串只能用 '...';"..." 是标识符 |
类型速查:建表时的映射
| MySQL 类型 | PostgreSQL 对应 | 备注 |
|---|---|---|
TINYINT(1) / BOOLEAN | boolean | PG 有真布尔,t/f |
INT, BIGINT | integer, bigint | PG 没有 UNSIGNED,用 CHECK (x>=0) |
DECIMAL(p,s) | numeric(p,s) | 钱用它,别用 float |
DATETIME / TIMESTAMP | timestamptz | 几乎总用带时区的那个 |
VARCHAR(n) / TEXT | text | PG 里长度不影响性能,多用 text |
ENUM('a','b') | CREATE TYPE ... AS ENUM | PG 的枚举是独立可共享类型 |
JSON | jsonb | 用二进制的 jsonb,能索引 |
BLOB | bytea | 二进制数据 |
| 逗号分隔的字符串 | text[] 数组 或 拆表 | PG 有真数组 |
UPSERT:从 ON DUPLICATE KEY 到 ON CONFLICT
「插入,如果已存在就更新」是高频操作。PostgreSQL 的写法更明确——你要指明基于哪个唯一约束判断冲突:
-- MySQL INSERT INTO stock (book_id, qty) VALUES (3, 10) ON DUPLICATE KEY UPDATE qty = qty + 10; -- PostgreSQL INSERT INTO stock (book_id, qty) VALUES (3, 10) ON CONFLICT (book_id) DO UPDATE SET qty = stock.qty + 10; -- 也能 DO NOTHING(冲突就跳过);EXCLUDED 指代「本来要插入的那行」
四个最容易栽的坑
PostgreSQL 会把不加引号的标识符统一折成小写。你写 CREATE TABLE Users (userName text),实际建的是 users(username)。而 MySQL 在 Linux 上表名大小写敏感、在 macOS/Windows 上不敏感(取决于文件系统)。麻烦在于:如果你加了双引号建表 CREATE TABLE "Users",那以后就必须每次都写 "Users",写 users 会找不到。建议:全用小写 + 下划线命名,永不加引号,一劳永逸。
MySQL 的 LIKE 默认不区分大小写(受排序规则影响);PostgreSQL 的 LIKE 区分大小写。要不区分,用 ILIKE,或用 citext 扩展类型(大小写不敏感的文本),或 lower(col) = lower(?)(记得配表达式索引)。迁移时所有依赖「不区分大小写模糊匹配」的查询都要检查。
MySQL(InnoDB) 默认 REPEATABLE READ,PostgreSQL 默认 READ COMMITTED(第 17 章)。如果你的业务逻辑依赖了 MySQL 默认给的「事务内两次读一致」,迁到 PG 的 RC 下会悄悄失效。要么显式把事务设成 REPEATABLE READ,要么改逻辑(比如用 SELECT ... FOR UPDATE 加锁)。
这是全书主线的回响:你在 MySQL 里习以为常的宽松——非法日期变 0000-00-00、字符串塞进 int 列、GROUP BY 里选没分组的列、超长字符串被截断——迁到 PostgreSQL 全会报错。迁移时你多半会撞见一批「原来一直在悄悄出错」的数据和查询。这不是 PG 在为难你,是它把你数据里一直存在、只是没人告诉你的问题,一次性暴露出来了。修掉它们,你的系统会更健康。
真要迁移一个 MySQL 库,别一张表一张表手动导。用 pgloader——它能连上 MySQL,自动做类型映射、建表、搬数据,还会把大部分上面的差异处理掉。一条命令起步:
$ pgloader mysql://user:pass@host/olddb postgresql://user:pass@host/newdb
迁完,务必用类生产数据跑一遍你的核心查询和 EXPLAIN,把报错的、变慢的逐个收拾——那份清单,往往就是你数据质量和查询习惯的一次体检报告。
收尾:为什么值得「当真」
二十四章走下来,我们其实只讲了一件事。
你最初对 PostgreSQL 的印象,也许只是「听说更强大」。现在你知道了,它强的不是功能列表更长,而是一种一以贯之的态度:它把你告诉它的每一件事都当真。你说这列是整数,它就绝不让一个 'abc' 混进来;你写下一条约束,它就替你挡住任何入口的违规;你开一个事务,它就保证全有或全无;你交给它一句查询,它就认认真真为你搜索最省的执行路径;并发再复杂,它用 MVCC 让每个人都看到一个一致而诚实的世界。
「当真」的代价,是你要在前期多花点心思——认真选类型、认真写约束、偶尔被一个报错拦下来当场修。但它的回报,是一样在数据世界里珍贵得不得了的东西:可信。
当数据库把每件事都当真,你就敢相信库里的每一个值——每个日期都是真日期,每笔余额都非负,每个订单都挂在真实的客户上,每一次并发都没骗过任何人。你不用再在应用层反复防御性校验,不用半夜被一个 0000-00-00 惊醒,不用对着一份对不上的报表怀疑人生。这份「敢相信」,就是「当真」最终交到你手里的礼物。
MySQL 是个好脾气的伙伴,很多场景它完全够用,你多年的 SQL 功底也一点不会浪费——它们在 PostgreSQL 上照样管用,只是更严谨了。这本书不是要你抛弃谁,是想让你在下一次建表、下一次写查询、下一次做技术选型时,多一个视角:我要不要让这件事,被当真?
去装一个 PostgreSQL,把你手头的项目建一张表试试。让它拒绝你一次脏数据——那一刻你会明白,被当真,是一种踏实。
— 全书完 · SERIOUS —
愿你的每一个值,都名副其实。