DeepSeek总结的ALTER FUNCTION SET work_mem:无需全局更改即可修复 PostgreSQL 磁盘溢出

ALTER FUNCTION SET work_mem:无需全局更改即可修复 PostgreSQL 磁盘溢出

D

Dinesh

2026 年 9 月 29 日

·

5 分钟阅读

一个订单 API 在旺季吞吐量降到一半,却没有部署可以归咎。原因是排序每天向磁盘溢出 150 GB。修复只需一行 SQL,作用域限定在一个函数上。

跟随部署而来的性能下降容易调查:你回滚并比较。没有部署作为背后原因的慢速则难得多,因为没有明显的变化可以指向。

这就是我们某个产品遇到的情况。在两天内,吞吐量(TPS)急剧下降,进入的订单降到通常量的大约一半。我们检查了每个代码仓库,发现前一周没有部署。当时还是节日季,我们一年中最忙的时候,所以我们需要在几小时内修复。

修复结果是一条语句:

sql 复制代码
ALTER FUNCTION fetch_all_products() SET work_mem TO '128MB';

复制

本文涵盖我们是如何走到这一步的、为什么我们没有为整个服务器提高 work_mem,以及如何验证函数级设置是否在按预期工作。

找到慢路径#

端到端 API 验证显示有一个 API 的响应远超我们可接受的 KPI 限制。

我们的大部分产品数据由 CDN 提供。当 CDN 缓存被失效时,第一个请求会到数据库重建内容。延迟就来自那个请求。

它指向构建产品目录的数据库函数 fetch_all_products()。它返回按类别分组并按排名排序的产品。按排名排序是常规操作。被排序的数据则不是:

  • 目录包含超过 10,000 个产品。
  • 每个产品行约 160 KB,因为产品描述和详情以内联的 base64 编码字符串存储。

这是生成 CDN 内容的查询:

sql 复制代码
WITH products AS (
    SELECT p.*,                                         -- 每个产品列
           (SELECT row_to_json(d) ...)    AS discount,
           (SELECT array_agg(t.name) ...) AS tags,
           (SELECT jsonb_agg(...) ...)    AS timings
    FROM product p
    JOIN category c ON p.category_id = c.id
    WHERE c.active AND p.active
)
SELECT c.name, c.tax_type, c.view_option, c.sort, c.img,
       jsonb_agg(row_to_json(p) ORDER BY p.sort) AS products   -- 瓶颈
FROM products p
JOIN category c ON p.category_id = c.id
GROUP BY 1, 2, 3, 4, 5;

复制

根本原因:一个溢出到磁盘的排序#

jsonb_agg 内部的 ORDER BY p.sort 看起来开销很小,因为它只按一个整数列排序。但 PostgreSQL 并不只排序键。它排序的是将要进入聚合的整行:每个产品列、base64 负载,以及计算出的 discount、tags 和 timings 值。

行这么宽,排序需要的内存远超默认的 work_mem(4 MB)。一旦排序超过 work_mem,PostgreSQL 就将其写入临时文件并在磁盘上继续。因此每次缓存失效都会引起一阵临时文件 I/O,推高了 API 延迟并拖慢了应用其余部分。

索引在这里帮不上忙。被排序的行是在查询期间构建的(来自 CTE、子查询和 row_to_json),而不是直接从表读取,因此没有索引可供计划器使用。这留下了两个选择:

  1. 重新设计 CDN 内容的生成方式。这是正确的长期修复,但在旺季中期太慢且风险太大。
  2. 通过提高 work_mem 给排序足够的内存以在 RAM 中完成。

为什么不全局提高 work_mem?#

work_mem 不是每连接限制。它是每个排序或哈希操作的限制,所以一个复杂查询可能使用它的好几倍,而每个并发连接都会得到同样的额度。全局提高得足够大以容纳这一个函数,可能会在峰值负载下把服务器推入内存压力,那只会用一个事故替换另一个事故。

我们只需要为一个函数提供额外内存。

修复:ALTER FUNCTION ... SET work_mem#

PostgreSQL 允许你将配置参数附加到函数上。该值在函数开始时生效,并在函数退出时恢复到之前的值。函数内的每个查询都使用新设置,而会话的其余部分以及每个其他会话都不受影响。

sql 复制代码
ALTER FUNCTION fetch_all_products() SET work_mem TO '128MB';

复制

该设置存储在函数的目录条目中,因此你可以确认它已附加:

sql 复制代码
SELECT proname, proconfig
FROM   pg_proc
WHERE  proname = 'fetch_all_products';

--       proname       |    proconfig
-- --------------------+------------------
--  fetch_all_products | {work_mem=128MB}

复制

证明设置保持在函数内部#

值得亲自看看作用域。这个小演示函数返回它运行时的 work_mem 值(在 PostgreSQL 17 上测试):

sql 复制代码
CREATE FUNCTION show_work_mem() RETURNS text
LANGUAGE sql AS $$ SELECT current_setting('work_mem') $$;

ALTER FUNCTION show_work_mem() SET work_mem TO '128MB';

SELECT current_setting('work_mem') AS session_work_mem,
       show_work_mem()             AS inside_function;

--  session_work_mem | inside_function
-- ------------------+-----------------
--  4MB              | 128MB

复制

在同一条语句中,会话报告 4 MB,而函数报告 128 MB。函数一返回,会话就回到 4 MB。

以后要移除该设置:

sql 复制代码
ALTER FUNCTION fetch_all_products() RESET work_mem;

复制

结果:每天 150 GB 降到零#

我们比较了更改前后 24 小时内该函数的临时文件写入:

阶段 函数 临时写入(24 小时) 调用次数
之前 fetch_all_products 150 GB 1,135
之后 fetch_all_products 0 GB 1,135

每天写入的临时文件

150 GB → 0 GB 同样是 1,135 次调用

一条 ALTER FUNCTION 语句,没有部署,没有全局配置更改。

函数被调用的次数相同,磁盘溢出从每天 150 GB 降到没有。API 延迟回到 KPI 限制内,应用恢复正常,每秒订单量也恢复了。

如果一个函数占了临时文件量的大部分,ALTER FUNCTION ... SET work_mem 就在问题所在处修复它,而不会让服务器其余部分承担风险。

相关推荐
咖啡八杯2 小时前
Redis 工具封装:RedisCache 对 Operations 体系的二次封装
java·数据库·redis·缓存·框架·springboot·若依
qq_452396232 小时前
第六篇:《SQL 优化的艺术:从慢查询到毫秒级响应》
数据库·sql
JavaPub-rodert3 小时前
王仕宇分享:把 MySQL 排障做成 AI Agent,慢 SQL、锁等待、连接数和主从异常都能自动查
人工智能·sql·mysql
ClouGence4 小时前
SQL 效率天花板!开源 CloudDM,自带一站式 AI SQL 助手
数据库·sql·开源
qq_452396234 小时前
第七篇:《事务与并发:MVCC、隔离级别与锁机制》
sql
Decay__4 小时前
附-01 · MySQL 压测与容量规划 —— sysbench 基准测试、容量评估与参数基线
运维·数据库·sql·mysql·架构
l1t4 小时前
DeepSeek总结的PostgreSQL 中的 SQL 与 PL/pgSQL:计划缓存
sql·缓存·postgresql
boy快快长大5 小时前
MySQL 转 PostgreSQL 使用指南
数据库·mysql·postgresql
ss2735 小时前
PostgreSQL咨询锁的使用
数据库·postgresql