在一台桌面机上,给 PostgreSQL 塞进 5 亿行订单

PostgreSQL 18.6 · 单张非分区表 · 16 并发 · 开启持久化 · 2026-09-14 实测

作者:Hanserwei。代码与完整结果:pg-single-table-bench。本文所有性能图表来自仓库的原始结果,不是示意数值。

graph TD A["同一台桌面机<br/>i9-14900K / 约 46 GiB RAM"] B["pgbench<br/>16 客户端 / 8 工作线程"] C["PostgreSQL 18.6<br/>WAL + fsync + 同步提交"] D["bench.orders<br/>5 亿行 / 4 个索引"] E["NVMe / Btrfs<br/>compress=zstd:3"] F["原始输出与统计快照<br/>1% 延迟采样 / 可复算报告"] A --> B B -->|Unix socket| C C --> D D --> E B --> F

“单表有 5 亿行,是不是就一定很慢?”

要回答这个问题,光看行数不够。按主键查一笔订单、取一个租户最新的 50 单、统计一小时的成交额,数据库要做的工作完全不同。再加上索引、缓存、磁盘和事务提交方式,同一个“TPS”背后可能是完全不同的实验。

所以我在自己的桌面机上实际生成了 5 亿行订单,没有分表,也没有分区,跑了一组读写场景。

先交代结果:主键查询约 4.86 万 TPS,逐事务持久化插入约 4,025 TPS,80% 读、20% 写混合约 8,404 TPS。整个流程从开始造数到最后一个报告生成,约 68 分钟

这些数字有明确前提:同机 Unix socket、16 并发、每场景测 60 秒、Btrfs 开启 zstd 压缩,没有清理操作系统页缓存。 这不是数据库极限排行榜,也不意味着真实业务可以直接照搬这个吞吐。

01 / 先把实验条件摆出来

不是服务器集群,就是一台桌面机

项目 实验配置
CPU Intel Core i9-14900K,24 核 / 32 逻辑 CPU,未绑核
内存 操作系统报告约 46 GiB 总内存,另有约 23 GiB zram swap
磁盘 TOPMORE Gemini NVMe,设备容量约 1.9 TiB
文件系统 Btrfs,compress=zstd:3
系统 Arch Linux,内核 7.2.4-arch1-2
PostgreSQL / pgbench 18.6 / 18.6
部署 数据库和压测客户端同机,使用私有 Unix socket

硬件和挂载信息是在压测完成后补采的,不是全过程监控。桌面程序和原有系统数据库没有被全部关闭,也没有记录整段 CPU 温度、频率或功耗变化。因此,本文不会把某一次波动武断归因于某个硬件指标。

写入不能靠关闭持久性拿高分

几个关键参数如下:

shared_buffers = '4GB'
effective_cache_size = '24GB'
work_mem = '16MB'
maintenance_work_mem = '512MB'

fsync = on
synchronous_commit = on
full_page_writes = on
wal_compression = on

max_wal_size = '8GB'
checkpoint_timeout = '15min'
checkpoint_completion_target = 0.9
jit = off

effective_cache_size 是优化器对可用缓存的估计,不是 PostgreSQL 又申请了 24 GiB 内存。work_mem 也不是整个数据库的内存上限,它与具体执行操作有关。

本次 wal_compression=on 实际使用 pglz。它压缩的是 WAL 中适用的内容,与文件系统的 zstd 压缩是两个层次,不能混为一谈。

表使用普通 LOGGED 存储,没有 UNLOGGED,也没有把 synchronous_commit 改成 off。完整参数和测量条件放在 README,运行时配置证据在 before.json

02 / 订单表不能只有 id 和一串随机文本

为了让查询有一点真实业务属性,我设计了一张 18 字段的订单表:

CREATE TABLE bench.orders (
    order_id bigint PRIMARY KEY,
    tenant_id integer NOT NULL,
    customer_id bigint NOT NULL,
    created_at timestamptz NOT NULL,
    updated_at timestamptz NOT NULL,
    paid_at timestamptz,
    status smallint NOT NULL CHECK (status BETWEEN 0 AND 5),
    channel smallint NOT NULL CHECK (channel BETWEEN 0 AND 3),
    region_id smallint NOT NULL,
    item_count smallint NOT NULL,
    total_amount numeric(12,2) NOT NULL,
    discount_amount numeric(12,2) NOT NULL,
    shipping_amount numeric(8,2) NOT NULL,
    currency varchar(3) NOT NULL DEFAULT 'CNY',
    shipping_city varchar(32) NOT NULL,
    payment_ref varchar(40),
    metadata jsonb NOT NULL,
    revision integer NOT NULL DEFAULT 0
) WITH (fillfactor=90);

这里省略了完整脚本中的 autovacuum 参数,实际 DDL 以 schema.sql 为准。

数据不是完全均匀分布:

  • 1,000 个租户中,20 个热点租户拥有约 80% 的订单。
  • 每租户最多 10,000 个合成客户,客户 ID 包含租户前缀。
  • 订单状态包含待支付、已支付、已发货、已完成、已取消、已退款,比例约为 5%、10%、15%、60%、7%、3%。
  • 金额、优惠、运费使用明确的小数类型;JSONB 保存活动、终端、会员和 SKU 信息。
  • 时间从 2024-01-01 UTC 开始覆盖 730 天,并按创建时间递增插入。

730 天不等于两个完整自然年。 2024 年是闰年,因此这份数据的时间上界在 2025-12-31 UTC 之前。

还有一个需要承认的局限:它依然是合成数据。部分字段复用哈希而存在相关性,状态更新也只是压测写放大的操作,不是真正完整的订单状态机。

4 个索引,各自服务一种访问路径

-- 主键索引由 PRIMARY KEY 创建
CREATE INDEX orders_customer_created_idx
ON bench.orders (tenant_id, customer_id, created_at DESC, order_id DESC);

CREATE INDEX orders_tenant_status_created_idx
ON bench.orders (tenant_id, status, created_at DESC, order_id DESC);

CREATE INDEX orders_created_brin_idx
ON bench.orders USING brin (created_at) WITH (pages_per_range=64);

客户列表和状态列表的索引没有把金额等返回字段全部 INCLUDE 进去,因此不是纯覆盖索引演示,仍然需要回表。

BRIN 则利用了时间和物理位置的相关性。它不是为每行保存完整定位项,而是记录一组相邻数据页的摘要。对于这种按时间追加的数据,可以用很小的索引排除大量不相关页。不过它是有损筛选,命中的页面仍要回表复查。官方 BRIN 文档也明确强调了物理相关性这个前提。

03 / 5 亿行,怎么生成才不会一次失败重头再来

我没有先生成一个巨大 CSV,再从客户端发送到 PostgreSQL。

做法是让服务器用 generate_series 直接生成数据,每批 25 万行,一共 2,000 批。每批的插入与进度标记在同一个事务里提交:

BEGIN;
-- 锁定进度记录
-- INSERT INTO bench.orders ... SELECT ... FROM generate_series(...)
-- UPDATE bench.bench_control SET loaded_rows = 本批最后一个ID
COMMIT;

上面只是展示事务边界,完整可执行版本在 load.sql

如果中断,当前未提交批次回滚;重启时从已提交的高水位继续。控制表仅保存进度,实际压测 SQL 仍只访问订单表,没有 JOIN。

主键在造数时就存在,另外三个索引在全量加载后构建。这一点非常重要:批量造数的每秒几十万行,不能拿来等同于业务逐笔 INSERT 的 TPS。 它们的事务大小、提交次数和索引维护成本完全不同。

真正花了多久

gantt title 真实阶段耗时(本机时间 UTC+8) dateFormat YYYY-MM-DD HH:mm:ss axisFormat %H:%M tickInterval 20minute section 数据准备 生成5亿行 :a, 2026-09-14 19:54:40, 2026-09-14 20:16:29 客户列表索引 :b, 2026-09-14 20:16:29, 2026-09-14 20:28:19 状态列表索引 :c, 2026-09-14 20:28:19, 2026-09-14 20:33:27 BRIN索引 :d, 2026-09-14 20:33:27, 2026-09-14 20:33:56 section 维护与测量 维护与检查点 :e, 2026-09-14 20:33:56, 2026-09-14 20:53:14 计划预热与压测 :f, 2026-09-14 20:53:14, 2026-09-14 21:02:36
阶段 本机时间 UTC+8 耗时
生成 5 亿行 19:54:40 → 20:16:29 21 分 49 秒
客户列表索引 20:16:29 → 20:28:19 11 分 50 秒
状态列表索引 20:28:19 → 20:33:27 5 分 8 秒
BRIN 索引 20:33:27 → 20:33:56 29 秒
VACUUM、ANALYZE、CHECKPOINT 20:33:56 → 20:53:14 19 分 18 秒
执行计划、预热与全部压测 20:53:14 → 21:02:36 9 分 22 秒

造数实际平均约 381,981 行/秒。阶段时刻来自 后台日志边界记录,时间轴按日志的秒级精度绘制。

为什么新表还要 VACUUM ANALYZE

大量插入之后,ANALYZE 会收集统计信息,让规划器了解列的分布、不同值数量、常见值等。没有这些信息,复杂度并不高的查询也可能选错计划。

VACUUM 除了回收不再需要的旧行版本,还会维护可见性映射等内部状态。这份刚加载的表通常没有很多死元组,因此不是为了“清掉大量垃圾”,也不是把表压缩成更小文件。

显式做这一步,是为了建立一个清楚的压测起点。但代价也必须记入整个准备流程,而不能只宣传造数速度。官方维护文档解释了 VACUUM 与统计维护的区别。

04 / 先看空间,再看吞吐

{ "animation": false, "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "grid": { "left": "3%", "right": "17%", "top": 24, "bottom": 55, "containLabel": true }, "xAxis": { "type": "value", "min": 0, "name": "关系逻辑大小 / GiB", "nameLocation": "middle", "nameGap": 30, "splitNumber": 3, "axisLabel": { "hideOverlap": true } }, "yAxis": { "type": "category", "inverse": true, "data": [ "表及辅助存储", "全部索引" ], "axisLabel": { "width": 100, "overflow": "break", "interval": 0 } }, "series": [ { "name": "逻辑大小", "type": "bar", "data": [ 117.2, 52.48 ], "barMaxWidth": 28, "label": { "show": true, "position": "right" } } ], "aria": { "enabled": true } }

压测开始前:

内容 PostgreSQL 报告的逻辑大小
表及表辅助存储 117.20 GiB
所有索引 52.48 GiB
合计 169.68 GiB

这里的数据来自 pg_table_sizepg_indexes_sizepg_total_relation_size,不是磁盘使用率截图。

文件系统开启了 zstd 压缩。 合成订单里有重复城市、状态和 JSON 键名,可能有较好的压缩性;但本次没有测量完整物理压缩率,也没有单独验证压缩带来的性能增益。因此,不能说“5 亿行业务数据只需要某个固定物理空间”,更不能拿它与未压缩文件系统直接比较。

05 / 8 个场景的最终结果

测试参数统一为:16 客户端、8 个 pgbench 工作线程、prepared 协议,每项预热 10 秒、测量 60 秒,单轮运行。

{ "animation": false, "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "grid": { "left": "3%", "right": "17%", "top": 24, "bottom": 55, "containLabel": true }, "xAxis": { "type": "value", "min": 0, "name": "脚本事务 / 秒", "nameLocation": "middle", "nameGap": 30, "splitNumber": 3, "axisLabel": { "hideOverlap": true } }, "yAxis": { "type": "category", "inverse": true, "data": [ "主键查单", "客户最新订单", "租户已支付订单", "一小时聚合", "新增订单", "普通更新", "索引更新", "读写混合" ], "axisLabel": { "width": 100, "overflow": "break", "interval": 0 } }, "series": [ { "name": "TPS", "type": "bar", "data": [ 48644.79, 16858.86, 45159.06, 420.58, 4024.59, 3858.05, 2494.61, 8403.97 ], "barMaxWidth": 28, "label": { "show": true, "position": "right" } } ], "aria": { "enabled": true } }
场景 TPS 平均延迟 ms 采样 p95 ms 采样 p99 ms
随机主键查单 48,644.79 0.329 0.619 0.714
客户最新 20 单 16,858.86 0.949 3.299 4.361
租户最新 50 条已支付订单 45,159.06 0.354 0.682 0.788
一小时订单分组聚合 420.58 38.009 87.273 92.248
插入一笔订单 4,024.59 3.974 4.261 7.231
修改普通字段 3,858.05 4.146 7.254 7.987
修改索引字段 2,494.61 6.412 8.122 11.072
80% 读 / 20% 写混合 8,403.97 1.903 5.912 8.561

8 个测量场景合计完成 7,793,274 次脚本事务,失败事务为 0。这个合计不包括预热,也不把客户端查询条数混算为事务数。

短查询与写入的采样尾延迟

{ "animation": false, "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "grid": { "left": "3%", "right": "17%", "top": 45, "bottom": 55, "containLabel": true }, "xAxis": { "type": "value", "min": 0, "name": "延迟 / ms", "nameLocation": "middle", "nameGap": 30, "splitNumber": 3, "axisLabel": { "hideOverlap": true } }, "yAxis": { "type": "category", "inverse": true, "data": [ "主键查单", "客户最新订单", "租户已支付订单", "新增订单", "普通更新", "索引更新", "读写混合" ], "axisLabel": { "width": 100, "overflow": "break", "interval": 0 } }, "series": [ { "name": "p95", "type": "bar", "data": [ 0.619, 3.299, 0.682, 4.261, 7.254, 8.122, 5.912 ], "barMaxWidth": 18 }, { "name": "p99", "type": "bar", "data": [ 0.714, 4.361, 0.788, 7.231, 7.987, 11.072, 8.561 ], "barMaxWidth": 18 } ], "aria": { "enabled": true }, "legend": { "data": [ "p95", "p99" ], "top": 0 } }

小时聚合的采样尾延迟

{ "animation": false, "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "grid": { "left": "3%", "right": "17%", "top": 45, "bottom": 55, "containLabel": true }, "xAxis": { "type": "value", "min": 0, "name": "延迟 / ms", "nameLocation": "middle", "nameGap": 30, "splitNumber": 3, "axisLabel": { "hideOverlap": true } }, "yAxis": { "type": "category", "inverse": true, "data": [ "一小时聚合" ], "axisLabel": { "width": 100, "overflow": "break", "interval": 0 } }, "series": [ { "name": "p95", "type": "bar", "data": [ 87.273 ], "barMaxWidth": 18 }, { "name": "p99", "type": "bar", "data": [ 92.248 ], "barMaxWidth": 18 } ], "aria": { "enabled": true }, "legend": { "data": [ "p95", "p99" ], "top": 0 } }

两图采用独立刻度;小时聚合只有 256 个采样,p99 不应当作稳定 SLA。

先别急着横向比较

“客户列表”和“租户列表”各执行两条 SELECT:第一条从随机订单中取客户/租户,第二条才是列表查询。这里报告的是整个脚本的吞吐和耗时,而不是只截取第二条 SQL。

1% 采样日志用于计算 p50/p95/p99,但 TPS 和平均延迟直接取 pgbench 原始输出。小吞吐的一小时聚合只获得 256 个延迟样本,其 p99 只是少数尾部样本的估计,不能当作稳定的 SLA。其他场景的采样数量也随报告一并公开。

06 / 结果里最值得读的,不是最大的数字

主键查询快,不意味着在内存里放下了 5 亿行

169.68 GiB 的关系逻辑大小明显大于机器物理内存,主键查询在 1 至 5 亿的种子范围中均匀随机抽样。

代表性执行计划使用 orders_pkey 的 Index Scan。并发压测期间,数据库统计也有明显的 shared block read 增量。但是 PostgreSQL 的块读取不必然等于物理 SSD 读取,操作系统页缓存还可能命中;更不能因为没有清缓存,就把它称为严格冷盘实验。

实测主键查询平均延迟为 0.329 ms,说明在这组索引和运行条件下,单表行数本身并没有让一次精确查找退化成扫描 5 亿行。

状态列表为什么接近主键查询的 TPS

它做的不是“扫描一个大租户所有已支付订单”,而是:

SELECT order_id, customer_id, total_amount, created_at
FROM bench.orders
WHERE tenant_id = :tenant AND status = 1
ORDER BY created_at DESC, order_id DESC
LIMIT 50;

复合索引匹配过滤和排序,找到足够结果就可以停止。同时,大多数请求会反复落到 20 个热点租户的最新结果区域,缓存局部性很好。

所以 4.52 万 TPS 描述的是 热点 Top-N 列表,不是一个“遍历大租户订单”的性能承诺。换成深 OFFSET、全量导出或更大时间范围,这个数字不能套用。

一小时聚合不是全表扫描,也不会像查一行那么轻

5 亿行均匀铺在 17,520 小时中,平均每小时约 28,539 行。

代表性计划走 BRIN Bitmap Index Scan,再做 Bitmap Heap Scan 和 HashAggregate;该计划实际返回 28,539 行到聚合前的扫描节点,还复查并排除了部分不匹配行。

HashAggregate
  -> Bitmap Heap Scan on orders
       -> Bitmap Index Scan on orders_created_brin_idx

这是“用较小索引缩小需要读取的物理范围,再处理几万行”的场景。平均约 38 ms、约 421 TPS,与单行精确读取是不同工作量,不能只看 TPS 排名判断哪条 SQL 写得差。

更新是否改索引,差距真的能看见

普通更新只修改 revisionupdated_at;状态更新还会修改出现在 B-tree 索引里的 status

更新吞吐

{ "animation": false, "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "grid": { "left": "3%", "right": "17%", "top": 24, "bottom": 55, "containLabel": true }, "xAxis": { "type": "value", "min": 0, "name": "TPS", "nameLocation": "middle", "nameGap": 30, "splitNumber": 3, "axisLabel": { "hideOverlap": true } }, "yAxis": { "type": "category", "inverse": true, "data": [ "普通字段", "索引字段" ], "axisLabel": { "width": 100, "overflow": "break", "interval": 0 } }, "series": [ { "name": "更新吞吐", "type": "bar", "data": [ 3858.05, 2494.61 ], "barMaxWidth": 28, "label": { "show": true, "position": "right" } } ], "aria": { "enabled": true } }

HOT 更新比例

{ "animation": false, "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "grid": { "left": "3%", "right": "17%", "top": 24, "bottom": 55, "containLabel": true }, "xAxis": { "type": "value", "min": 0, "name": "%", "nameLocation": "middle", "nameGap": 30, "splitNumber": 3, "axisLabel": { "hideOverlap": true } }, "yAxis": { "type": "category", "inverse": true, "data": [ "普通字段", "索引字段" ], "axisLabel": { "width": 100, "overflow": "break", "interval": 0 } }, "series": [ { "name": "HOT 更新比例", "type": "bar", "data": [ 100.0, 0.0 ], "barMaxWidth": 28, "label": { "show": true, "position": "right" } } ], "aria": { "enabled": true } }

实例 WAL 增量

{ "animation": false, "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "grid": { "left": "3%", "right": "17%", "top": 24, "bottom": 55, "containLabel": true }, "xAxis": { "type": "value", "min": 0, "name": "MiB", "nameLocation": "middle", "nameGap": 30, "splitNumber": 3, "axisLabel": { "hideOverlap": true } }, "yAxis": { "type": "category", "inverse": true, "data": [ "普通字段", "索引字段" ], "axisLabel": { "width": 100, "overflow": "break", "interval": 0 } }, "series": [ { "name": "实例 WAL 增量", "type": "bar", "data": [ 578.0, 1742.09 ], "barMaxWidth": 28, "label": { "show": true, "position": "right" } } ], "aria": { "enabled": true } }

本次场景前后统计的增量如下:

指标 普通字段更新 索引字段更新
更新行数增量 232,732 150,147
HOT 更新增量 232,732 0
吞吐 3,858.05 TPS 2,494.61 TPS
同一测量窗口实例 WAL 字节增量 606,078,763 1,826,708,964

这次普通更新的 HOT 比例达到 100%,状态更新为 0。普通更新不修改索引列,而 fillfactor=90 又为同页新版本留出空间,符合 HOT 的触发条件

索引字段更新的吞吐约低 35.3%。WAL 也更多,但这些是实例级计数器,可能包含全页镜像和其他后台影响。两组测试顺序执行,并没有恢复成同一快照,所以不能把所有差异精确归因于“多维护一个索引”。

正确的结论是:是否能走 HOT,以及更新会触及哪些索引,值得在自己的业务表上单独测。 不是“所有普通字段更新都能达到 100% HOT”。

混合负载的平均延迟会掩盖读写差异

混合场景权重为:主键读 60%、客户列表 15%、状态列表 5%、插入 10%、普通更新 5%、状态更新 5%。80/20 按脚本事务计,不按 SQL 条数计。

其平均延迟约 1.903 ms,但采样 p50 约 0.382 ms,p99 约 8.561 ms。读操作较快、写操作需要持久化提交,混在一起之后,一个平均值无法代表两类请求各自的体验。

真实接口有延迟目标时,最好进一步拆开混合场景里的读写分位数,而不是只给整个系统贴一个“平均 2 ms”的标签。

07 / 哪些结论,这次实验不能给

  1. 不能推断生产峰值。 只有 16 并发,没有完整并发阶梯,也没有接近真实流量的固定到达率模型。
  2. 不能承诺长时间稳态。 每场景只跑 60 秒,未充分覆盖 checkpoint、autovacuum、持续更新膨胀与 SSD 长时间写入行为。
  3. 不能当作冷缓存成绩。 建索引、维护、EXPLAIN、预热和前面的场景都会改变缓存状态。
  4. 不能比较文件系统或硬件优劣。 只跑了这一台机器、这一种挂载条件,没有关闭压缩的受控对照。
  5. 不能将小样本成绩乘除外推。 100 万行样本能驻留内存;它只用于验证脚本与估算容量,和本次全量并发、测量时间也不同。
  6. 不能把零失败理解为业务正确性认证。 它意味着这次 pgbench 没有失败事务,不代表订单状态机、支付流程和生产恢复机制已得到验证。
  7. 不能将 EXPLAIN 的单次执行时间当成压测延迟。 公开的是代表性字面量计划,不是整个 prepared 并发运行的通用计划证明。

公开这些限制,不会让实验失去价值。它让读者知道哪些数字可以参考,哪些必须在自己的环境中重新测。

08 / 自己复现

需要普通 Linux 用户、Python,以及同一主版本的 PostgreSQL 18 工具链。完整压测本身只有 Python 标准库依赖,不需要安装数据库驱动,也不要求 Docker。

先跑 100 万行样本,确认空间,再做全量:

git clone https://github.com/Hanserwei/pg-single-table-bench.git
cd pg-single-table-bench

python3 bench.py start
python3 bench.py --db bench_pilot init --rows 1000000
python3 bench.py --db bench_pilot load
python3 bench.py --db bench_pilot prepare
python3 bench.py --db bench_pilot estimate

再执行这次完整配置:

python3 bench.py --db bench_orders pipeline \
  --rows 500000000 --batch-size 250000 \
  --clients 16 --jobs 8 --seconds 60 --warmup 10 --sample-rate 0.01

脚本会创建当前用户自己的实例,数据放在仓库 data/ 中,不改现有 5432 数据库。默认保留 100 GiB 磁盘余量,但这只是操作边界检查,不是实时磁盘配额。大批量实验之前仍要为 WAL、索引排序、膨胀和同盘其他文件留足空间。

结果不只是一张表。仓库附有:

  • 8 个场景的原始 pgbench 输出、1% 事务采样日志与汇总 CSV/JSON。
  • 参数、表/索引大小、WAL/I/O 和每场景前后统计快照。
  • 代表性执行计划、阶段边界日志,以及可复算图表的生成脚本。
  • 原始源码摘要与结果校验和;公开文本中的本机绝对工程路径替换为 <repo>,数值不改。

不必相信文章里的手抄数字,可以直接复算:

python3 -m unittest -v test_bench.py
python3 tools/verify_report.py

仓库的 CI 只做测试与历史报告复核,不会在 GitHub Actions 上再造一次 5 亿行。

结语

这次实验没有得到一个“PostgreSQL 到多少行就必须分表”的神奇阈值。

它得到的是更具体的观察:5 亿行不是查询复杂度本身。 主键精确查找、按复合索引取热点 Top-N、按时间摘要定位几万行再聚合,以及维护索引并提交 WAL,是四种不同的工作。

决定查询体验的,不只是数据有多少,还包括请求需要访问多少数据、访问路径能否提前停止、缓存覆盖的是哪一部分,以及一次更新会带来多少额外写入。

下一步值得做的,也不是急着把这次数字包装成结论,而是固定住环境,分别改变并发、缓存状态、索引和数据分布,再重复测量。


代码与报告: Hanserwei/pg-single-table-bench

参考资料: pgbench · HOT · BRIN · VACUUM