先说结论,别急着抄。我把 PostgreSQL 16.2 和 MySQL 8.0.36 装在同一台阿里云 ecs.g7.8xlarge 上,32 vCPU Intel Xeon Platinum 8269CY,64GB DDR4,ESSD PL1 1TB,Ubuntu 22.04.4 LTS,内核 5.15.0-105-generic。PG 编译安装,shared_buffers=16GB,work_mem=64MB,maintenance_work_mem=2GB,effective_cache_size=48GB,max_wal_size=8GB,autovacuum_vacuum_scale_factor=0.02。MySQL 8.0.36,innodb_buffer_pool_size=32GB,innodb_log_file_size=2GB,innodb_flush_log_at_trx_commit=1,binlog 关掉。数据集 2000 万行 events,payload 平均 1.1KB,字段有 user_id、device.os、tags 数组 3-8 个、geo 嵌套。我本来想跑 5000 万行,结果磁盘只剩 180GB,怂了,改成 2000 万行。
建表和索引,我踩的第一个坑
PG 这边:
create table events (
id bigserial primary key,
payload jsonb not null,
created_at timestamptz default now()
);
create index idx_payload_gin on events using gin(payload jsonb_path_ops);
create index idx_payload_gin_default on events using gin(payload);
第一个 GIN 用 jsonb_path_ops,大小 3.8GB,构建 18分42秒;第二个默认 GIN,大小 5.6GB,构建 26分11秒。为什么建两个?因为 jsonb_path_ops 不支持 ?、?|、?&,我查 tags 时又补了默认 GIN。这个坑我第一次真没注意,查文档才发现。
MySQL 这边:
create table events (
id bigint auto_increment primary key,
payload json not null,
created_at datetime(6) default current_timestamp(6)
);
alter table events add column user_id varchar(32) generated always as (json_unquote(json_extract(payload, '$.user_id'))) stored;
create index idx_user_id on events(user_id);
alter table events add index idx_tags ((cast(json_extract(payload, '$.tags') as char(32) array)));
多值索引要求 JSON 是数组,tags 里如果有 null 会报错,我插数据时清洗了一遍。语法也挑版本,8.0.17 以后才有,8.0.36 没问题。
7 组查询结果,别只看 QPS
我跑了 7 组,每组的缓存都预热两次,取第三次。下面这些数字是同机同数据下的中位数,不是官方 benchmark。
| 查询 | PG 无索引 | PG GIN/表达式 | MySQL 无索引 | MySQL 生成列/多值 |
|---|---|---|---|---|
| user_id 精确 | 14.8s | 0.29s(@>)/0.004s(表达式索引) | 13.6s | 0.003s |
| device.os=iOS | 12.1s | 0.42s | 12.3s | 0.006s |
| tags 包含 vip | 11.7s | 0.08s | 11.9s | 0.07s |
| geo 嵌套范围 | 15.2s | 1.1s | 16.4s | 0.9s |
| 多条件组合 | 17.5s | 2.4s | 18.1s | 1.7s,FORCE INDEX 后 0.6s |
| count 聚合 | 9.8s | 3.8s | 10.2s | 4.2s |
| 写入 20 万行 | 1.8万行/s | 6200行/s | 2.3万行/s | 8900行/s |
更新 10 万行 jsonb_set/json_set:PG 43s,表膨胀 1.7GB;MySQL 31s,但索引维护更重,innodb_buffer_pool 脏页飙升。存储:PG 表 28GB,TOAST 9GB,GIN 5.6GB;MySQL 表 26GB,生成列+索引 4.2GB。
我的独立观点:别把 JSONB 当 MongoDB 用
如果你只是 JSON 里固定几个字段,别学我,拆列。JSONB 适合动态属性,不适合主查询路径。PG 的 GIN 更成熟,但写放大和 vacuum 痛;MySQL 多值索引在数组查询上快,但语法恶心、优化器容易抽风。我生产最后选了 PG,但把 tags 拆成 event_tags 关联表,user_id、device_os 单独列,JSONB 只存原始事件。这不是标准答案,是我被两边都打脸后的选择。
复现步骤:
- 准备 Ubuntu 22.04,装 PG 16.2 和 MySQL 8.0.36。
- 按上面建表,PG 建 jsonb_path_ops GIN 和默认 GIN,MySQL 建生成列和多值索引。
- 用 pgbench 或 sysbench 造 2000 万行,payload 1.1KB,tags 3-8 个。
- 用 explain (analyze, buffers) 和 MySQL EXPLAIN ANALYZE 看执行计划。
- 写入测试开 32 并发,每批 1000 行,跑 20 万行。
- 更新测试随机选 10 万行,PG 用 jsonb_set,MySQL 用 json_set。
- 记录 iostat、pg_stat_user_tables、innodb_metrics。
别拿我的数字当标准答案,你的数据分布一变,结论可能反过来。我这次被 MySQL 多值索引打脸,也被它优化器坑过,真实感受就是:基础软件选型,benchmark 只能信一半,剩下一半得自己踩坑。