How to Schedule Jobs in PostgreSQL with pg_cron
TL;DR · AI 摘要
pg_cron 是一个 PostgreSQL 扩展,允许通过 SQL 调度任务,无需外部进程。
核心要点
- pg_cron 是一个 PostgreSQL 扩展,允许通过 SQL 调度任务。
- 使用 pg_cron 可以避免外部进程和额外的凭证管理。
- pg_cron 支持 PostgreSQL 10 及以上版本。
结构提纲
按章节快速跳转。
- §引言
介绍在后端系统中调度任务的常见方法及其局限性。
列出跟随教程所需的先决条件,包括 SQL 知识和 PostgreSQL 实例。
介绍 pg_cron 是一个 PostgreSQL 扩展,允许通过 SQL 调度任务。
解释 pg_cron 在 PostgreSQL 启动时如何启动一个后台工作线程。
介绍如何安装和配置 pg_cron 扩展。
演示如何使用 SQL 语句调度第一个任务。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- pg_cron
- 功能
- 调度 SQL 任务
- 无需外部进程
- 安装
- PostgreSQL 10+
金句 / Highlights
值得收藏与分享的关键句。
pg_cron 是一个 PostgreSQL 扩展,允许通过 SQL 调度任务,无需外部进程。
使用 pg_cron 可以避免外部进程和额外的凭证管理。
pg_cron 支持 PostgreSQL 10 及以上版本。
如何在 PostgreSQL 中使用 pg_cron 安排任务
2026 年 6 月 17 日
/
#PostgreSQL
iyiola
每个后端系统最终都需要某种方式来按计划运行任务。旧的会话需要删除,摘要表需要重建,物化视图需要刷新,维护任务需要在所有人都睡觉时执行。
通常的解决方案是使用数据库外部的工具:系统 crontab、Kubernetes CronJob、Celery beat 工作器或调度服务。所有这些方法都可以工作,但它们会增加额外的组件。现在你还需要管理凭证,监控一个独立的进程,还有一个可能在静默中停止运行的组件。
pg_cron 采用了一种不同的方法。它是一个 PostgreSQL 扩展,它在数据库内部运行一个类似 cron 的调度器。你可以使用普通的 SQL 来安排任务,数据库会执行它们,运行历史记录会存储在一个你可以像查询其他任何内容一样查询的表中。
在本教程中,你将学习 pg_cron 的工作原理,如何安装和配置它,以及如何使用它执行实际的维护任务。你还将学习如何监控任务,管理权限,以及决定何时使用 pg_cron 是合适的工具,何时不使用它。
目录
- 先决条件
- 什么是 pg_cron?
- pg_cron 的工作原理
- 如何安装和设置 pg_cron
- 有关 cron 语法的快速复习
- 如何安排第一个任务
- 实用的 pg_cron 示例
- 如何查看和监控任务
- 如何更新和删除任务
- 如何在其他数据库中运行任务
- 如何让其他用户安排任务
- 何时使用 pg_cron(以及何时避免使用它)
- 与 pg_cron 一起工作的最佳实践
- 结论
先决条件
要跟随示例,你需要:
- 基本的 SQL 知识(SELECT、INSERT、UPDATE、DELETE)
- 一个正在运行的 PostgreSQL 实例(版本 13 或更高版本是理想选择,尽管 pg_cron 支持版本 10 及以上)
- 对该实例的超级用户或管理员访问权限,因为安装扩展需要它
- 一个 SQL 客户端,如 psql、pgAdmin 或 DBeaver
如果你没有运行自己的服务器,那也没关系。大多数托管 PostgreSQL 服务(包括 Amazon RDS、Azure Database for PostgreSQL、Google Cloud SQL、Supabase 和 Neon)都支持 pg_cron。我将在教程的后面部分介绍如何在这些服务上启用它。
什么是 pg_cron?
pg_cron 是一个开源的 PostgreSQL 扩展,最初由 Citus Data 团队构建,它允许你使用熟悉的 cron 语法来安排 SQL 命令。
你不需要在服务器上编写 crontab 条目,而是编写一个 SQL 语句:
SELECT cron.schedule(
'nightly-cleanup',
'0 3 * * *',
$$DELETE FROM sessions WHERE expires_at < now()$$
);该语句告诉 PostgreSQL 每天凌晨 3 点删除过期的会话。不需要外部进程,不需要 shell 脚本,不需要额外的凭证。任务定义存储在数据库中,如果需要,可以与你的迁移脚本一起进行版本控制。
由于调度器只是一个扩展,你的任务会随着数据库一起移动。任何可以连接和查询的人都可以看到确切的安排内容、上次运行时间以及是否成功。
当任务的计划时间到达时,worker 会执行命令。默认情况下,它会像你的应用程序一样,通过打开一个新的本地数据库连接来执行。你也可以配置它使用 PostgreSQL 的后台 worker 而不是连接,我会在设置部分向你展示。
有两项行为在开始之前值得了解:
首先,pg_cron 可以并行运行许多不同的任务,但它绝不会同时运行同一任务的两个实例。如果任务在下一个计划时间到达时仍在运行,新的运行将等待在队列中,并在当前运行完成后立即开始。这可以保护你免受缓慢的清理任务不断堆积的问题。
其次,pg_cron 在服务器处于热备用模式时不会运行任务。如果你使用流复制,任务只会在主服务器上执行。当一个副本被提升为主服务器时,调度器会自动启动 —— 这样故障转移不会导致你失去计划任务。
如何安装和设置 pg_cron
在自托管服务器上设置 pg_cron 需要三个步骤:安装软件包、更新配置和创建扩展。
第一步:安装软件包
使用官方 PostgreSQL apt 仓库在 Debian 或 Ubuntu 上安装与你的 PostgreSQL 主版本匹配的软件包。对于 PostgreSQL 17,命令如下:
sudo apt-get install postgresql-17-cron在使用 PGDG yum 仓库的基于 Red Hat 的系统上:
sudo yum install pg_cron_17如果你使用的是 PostgreSQL 16 或 18,请相应地更改版本号。如果平台没有现成的软件包,你也可以从源代码构建扩展。
第二步:更新 postgresql.conf
pg_cron 需要在 PostgreSQL 启动时启动其后台 worker,因此它必须被预加载。在你的 postgresql.conf 中添加以下内容:
shared_preload_libraries = 'pg_cron'如果该设置已经列出了其他库,请将 pg_cron 添加到逗号分隔的列表中,而不是替换它们。
默认情况下,调度器会将它的元数据存储在名为 postgres 的数据库中。如果你的应用程序位于不同的数据库中,并且你希望任务在该数据库中运行,请设置:
cron.database_name = 'app_db'另一个值得了解的设置是:pg_cron 默认将所有计划时间解释为 GMT。如果你想让“凌晨 3 点的清理”任务实际在本地时间的凌晨 3 点运行,请显式设置时区:
cron.timezone = 'Africa/Lagos'这些设置需要重启服务器才能生效:
sudo systemctl restart postgresql第三步:创建扩展
连接到你在 cron.database_name 中配置的数据库,并以超级用户身份创建扩展:
CREATE EXTENSION pg_cron;这会创建 cron 架构、元数据表和调度函数。你现在可以开始安排任务了。
请注意,pg_cron 在每个 PostgreSQL 实例中只能安装在一个数据库中。这听起来有些限制,但其实不然。你仍然可以使用 cron.schedule_in_database() 在实例上的任何数据库中运行任务,我们稍后会介绍这一点。
关于任务连接方式的说明
由于 pg_cron 默认会打开本地连接,因此你的 pg_hba.conf 需要允许这些连接。常见的方法是为任务用户启用本地连接的信任认证,或者将密码放在 .pgpass 文件中。
如果你想完全避免连接认证,请告诉 pg_cron 使用后台 worker:
cron.use_background_workers = on
max_worker_processes = 20使用后台工作进程时,可以同时运行的任务数量由 max_worker_processes 限制,因此如果你安排了大量重叠的任务,请提高该值。
在托管数据库服务上使用 pg_cron
如果你使用的是托管服务,通常无法直接编辑 postgresql.conf,但提供商通过自己的机制暴露了相同的设置:
- Amazon RDS 和 Aurora PostgreSQL:在数据库参数组的 shared_preload_libraries 参数中添加 pg_cron,重启实例,然后以具有 rds_superuser 权限的用户身份运行
CREATE EXTENSION pg_cron;。调度器在 postgres 数据库中运行。
- Azure Database for PostgreSQL:在服务器参数中启用 pg_cron(shared_preload_libraries 和 azure.extensions),重启后创建扩展。
- Google Cloud SQL:设置 cloudsql.enable_pg_cron 标志,重启后创建扩展。
- Supabase:在数据库 → 扩展的仪表板中通过一个切换按钮启用 pg_cron 扩展。
- Neon:pg_cron 是一个支持的扩展,可以按项目启用。
之后编写的 SQL 在所有地方都是一样的,这也是其吸引力之一。
关于 Cron 语法的快速回顾
pg_cron 使用与经典 Unix cron 相同的五字段计划格式:
┌──────────── minute (0–59)
│ ┌────────── hour (0–23)
│ │ ┌──────── day of month (1–31, or $ for the last day)
│ │ │ ┌────── month (1–12)
│ │ │ │ ┌──── day of week (0–6, Sunday = 0)
│ │ │ │ │
* * * * *星号表示“所有值”。你可以用逗号组合值,用连字符表示范围,用斜杠表示步长。一些你经常使用的计划包括:
*/5 * * * * 每 5 分钟
0 * * * * 每小时,整点
0 3 * * * 每天凌晨 3:00
0 3 * * 1-5 每个工作日的凌晨 3:00
30 1 * * 0 每周日的凌晨 1:30
0 0 1 * * 每月 1 日的午夜pg_cron 还在标准语法中添加了两个扩展,这是普通 cron 所没有的。
你可以在月份的字段中使用 $ 表示月份的最后一天,这在标准 cron 中表达起来非常麻烦:
0 23 $ * * 每月最后一天的晚上 11:00对于需要比每分钟更频繁运行的任务,你可以使用 1 到 59 秒之间的普通间隔:
'30 seconds' 每 30 秒秒的语法是独立的 —— 你不能将其与五字段格式混合使用。
如果你对某个计划的含义有任何疑问,crontab.guru 可以将 cron 表达式翻译成普通英文。请记住,pg_cron 会根据 cron.timezone 设置的时区来评估计划,其默认值为 GMT。
如何安排你的第一个任务
核心函数是 cron.schedule()。它有两种形式:一种带有名称,一种不带名称。
你应该使用带名称的形式,因为名称使任务易于查找、更新和删除:
SELECT cron.schedule(
'delete-expired-sessions', -- 任务名称
'0 3 * * *', -- 计划
$$DELETE FROM sessions WHERE expires_at < now()$$ -- 命令
);该函数返回任务的 ID:
schedule
----------
1
(1 row)一些值得关注的细节:
命令被包裹在 $$ ... $$ 中,这是 PostgreSQL 的美元引号。这可以避免在 SQL 中转义单引号。对于没有引号的命令,普通字符串字面量也可以正常工作。
任务在你调用 cron.schedule() 的数据库中运行,以你调用它时使用的用户身份运行,并使用该用户的正常权限。调度器中没有隐藏的权限提升 —— 如果你的用户无法从 sessions 中删除数据,任务也无法做到这一点。
如果你使用相同的任务名称再次调用 cron.schedule(),pg_cron 会更新现有的任务,而不是创建一个副本。这使得调度具有幂等性,这在数据库迁移中定义任务时非常方便。
实用的 `pg_cron` 示例
让我们一起看看覆盖大多数实际应用场景的模式。每个示例都可以直接进行调整。
示例 1:每天清理旧的行
收集临时数据的表(如会话、令牌、审计事件、通知日志)如果没有被修剪,它们会无限增长。每天一次的删除操作是经典的 pg_cron 任务:
SELECT cron.schedule(
'purge-old-events',
'0 2 * * *',
$$DELETE FROM events WHERE created_at < now() - interval '90 days'$$
);每天凌晨 2:00,超过 90 天的行将被删除。如果表很大,建议在函数中分批次删除,以确保每次运行时间较短,然后安排该函数的执行。
示例 2:每小时刷新一个物化视图
物化视图是缓存昂贵聚合查询的一种好方法,但 PostgreSQL 本身不会自动刷新它们。pg_cron 解决了这个问题:
SELECT cron.schedule(
'refresh-daily-sales',
'5 * * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales_summary'
);这会在每小时的第 5 分钟刷新视图。CONCURRENTLY 选项允许在刷新过程中继续读取,前提是视图有一个唯一索引。
示例 3:构建每日汇总表
汇总表是另一个常见的模式:而不是在每次加载仪表板时对数百万条原始数据进行聚合,你可以在每天预计算这些数字。
SELECT cron.schedule(
'rollup-daily-orders',
'15 0 * * *',
$$
INSERT INTO daily_order_stats (day, order_count, total_amount)
SELECT created_at::date, count(*), sum(amount)
FROM orders
WHERE created_at >= current_date - 1
AND created_at < current_date
GROUP BY created_at::date
ON CONFLICT (day) DO UPDATE
SET order_count = EXCLUDED.order_count,
total_amount = EXCLUDED.total_amount
$$
);在午夜 15 分钟后,前一天的订单将被汇总成一行。ON CONFLICT 子句使任务可以安全地重新运行 —— 如果任务执行了两次,它会覆盖而不是重复。
示例 4:每 30 秒运行一次任务
有些工作需要比 cron 的一分钟最低频率更频繁地执行:刷新缓冲表、从出站队列中获取行、推进轻量级队列。秒语法可以处理这种情况:
SELECT cron.schedule(
'process-outbox',
'30 seconds',
'CALL process_outbox_batch()'
);记住前面的保证:pg_cron 不会在第一个实例仍在运行时启动该任务的第二个实例。如果一个批次偶尔需要 45 秒,下一次运行只是等待其轮到自己,而不是争抢执行。
示例 5:在每月最后一天运行维护任务
标准 cron 在处理每月最后一天的任务时比较繁琐,因为月份的长度各不相同。pg_cron 的 $ 语法使这变得非常简单:
SELECT cron.schedule(
'month-end-vacuum',
'0 23 $ * *',
'VACUUM ANALYZE orders'
);这会在 28 日、29 日、30 日或 31 日(视具体月份而定)的晚上 11:00 对 orders 表执行 VACUUM ANALYZE。
如何查看和监控任务
pg_cron 所有信息都存储在 cron 模式下的两个表中,你可以像查询其他表一样查询它们。
要查看已计划的任务,请查看 cron.job:
SELECT jobid, jobname, schedule, command, active
FROM cron.job;jobid | jobname | schedule | command | active
-------+-------------------------+------------+--------------------------------+--------
1 | delete-expired-sessions | 0 3 * * * | DELETE FROM sessions WHERE ... | t
2 | refresh-daily-sales | 5 * * * * | REFRESH MATERIALIZED VIEW ... | t
(2 rows)要查看任务实际的运行情况,请查询 cron.job_run_details:
SELECT jobid, status, return_message, start_time, end_time
FROM cron.job_run_details
ORDER BY start_time DESC
LIMIT 10;每一行记录一次执行情况:执行是否成功或失败,返回的信息,以及开始和结束的时间。一个失败的任务会显示 status = 'failed' 以及错误信息,因此调试通常从这张表开始和结束。
一个重要的注意事项是:pg_cron 从不会自动清理这张表。一个每30秒运行一次的任务每天会写入近三千行数据。标准的解决方法非常巧妙地递归——安排一个 pg_cron 任务来清理 pg_cron 自己的历史记录:
SELECT cron.schedule(
'purge-cron-history',
'0 12 * * *',
$$DELETE FROM cron.job_run_details
WHERE end_time < now() - interval '14 days'$$
);如果你根本不想记录运行历史,可以在配置中设置 cron.log_run = off。
如何更新和删除任务
要更改现有任务,使用 cron.alter_job() 并传入任务的 ID。只有你传递的参数会被更改,其余所有内容保持不变:
-- 将任务 1 从凌晨 3 点调整到凌晨 4 点
SELECT cron.alter_job(1, schedule := '0 4 * * *');
-- 暂停任务而不删除它
SELECT cron.alter_job(1, active := false);
-- 后续恢复任务
SELECT cron.alter_job(1, active := true);使用 active := false 暂停任务是一个被低估的功能。在发生事故或进行大规模迁移时,你可以关闭一个嘈杂的任务,之后再重新开启,而不会丢失其定义。
要永久删除一个任务,使用 cron.unschedule(),并传入任务的名称或 ID:
SELECT cron.unschedule('delete-expired-sessions');
-- 或
SELECT cron.unschedule(1);当任务被找到并删除时,两者都会返回 true。
如何在其他数据库中运行任务
请记住,pg_cron 在每个实例中只安装在一个数据库中,通常是 postgres。如果你的实例托管了多个数据库,你不会在每个数据库中都安装 pg_cron,而是从它所在的位置安排跨数据库任务,使用 cron.schedule_in_database():
SELECT cron.schedule_in_database(
'analytics-nightly-vacuum',
'0 4 * * *',
'VACUUM ANALYZE page_views',
'analytics_db'
);任务是集中存储的,但会在 analytics_db 中执行。该函数还接受一个可选的用户名,如果任务应以不同的用户身份运行,以及一个 active 标志,如果你想创建一个暂停的任务。
这种模式将所有调度集中在同一个数据库的同一个模式中,使得审计变得简单:只需执行一次 SELECT * FROM cron.job 即可查看整个实例中的所有计划任务。
如何让其他用户安排任务
默认情况下,只有超级用户可以调用调度函数。为了让一个应用程序角色管理自己的任务,授予其对 cron 模式的使用权限:
GRANT USAGE ON SCHEMA cron TO app_user;之后的权限模型是合理且安全的:
- 作业以安排它们的用户的权限运行,仅此而已。
- 在
cron.job上设置行级安全策略意味着用户只能查看和修改自己的作业。超级用户可以看到所有内容。
- 每个用户还可以从
cron.job_run_details中删除自己的行,因此前面提到的清理作业无需超级用户权限即可运行。
在实践中,我建议为计划任务创建一个专用角色,而不是以个人账户身份安排作业。当安排所有任务的工程师离职,其角色被删除时,你不希望这些夜间汇总任务也随之消失。
何时使用 pg_cron(以及何时避免使用它)
当工作是数据库相关的工作时,pg_cron 表现出色。适用于以下场景:
- 数据保留:从会话、日志、事件和令牌表中删除旧的行。
- 聚合:刷新物化视图和构建汇总表。
- 维护:定向执行
VACUUM ANALYZE,重建统计信息,管理分区(它与pg_partman配合得非常好)。
- 轻量级流水线:在表之间移动行,处理出站模式,过期软删除的记录。
共同点:整个作业可以表示为 SQL 或存储过程,并且不涉及数据库之外的任何内容。
你应该使用其他工具的情况包括:
- 作业需要调用外部系统。pg_cron 运行 SQL。它无法发送 HTTP 请求、推送到队列或自行发送电子邮件。这类作业应属于你的应用程序或工作流引擎。
- 你需要内置重试、退避和告警功能。pg_cron 记录失败,但不会重试或通知你。对于必须完成的工作流,像 Temporal 这样的工具或合适的作业队列值得其复杂性。
- 工作负载繁重且运行时间长。在主 OLTP 数据库中运行一个四小时的批处理作业会与你的应用程序争夺 CPU、内存和锁。将繁重的计算任务安排在其他地方进行。
- 作业需要复杂的依赖关系。例如,“只有在 A 成功后运行 B,然后分发到 C 和 D”属于编排任务。这是 Airflow 的领域,而不是 cron 的领域。
一个合理的经验法则:pg_cron 替代的是以前在某个被遗忘的服务器上运行 psql -c "..." 的 crontab 条目。它不会替代你的作业队列或工作流编排器。
使用 pg_cron 的最佳实践
一些习惯可以让你的计划任务变得无趣,从最好的意义上来说:
为每个作业命名:仅通过 ID 识别的匿名作业在六个月后管理起来非常痛苦。名称还能使 cron.schedule() 具有幂等性,从而让你在迁移中安全地定义作业。
明确设置时区:默认是 GMT,通过在第一天设置 cron.timezone,你可以避免“为什么凌晨 3 点的作业在凌晨 4 点运行?”这种问题。
保持单次运行时间简短:将大删除操作包装在分批存储过程中。在几秒钟内完成的作业只会短暂持有锁,并且不会在后面排队。
使作业具有幂等性:服务器重启时,作业可能在中途失败。使用 ON CONFLICT、时间窗口谓词和其他使重新运行无害的模式。
清理 cron.job_run_details:在监控部分之前安排清理作业,以防止表变得太大,直到你以困难的方式注意到它。
监控静默,而不仅仅是失败:失败的运行会在 job_run_details 中显示,但一个完全停止安排的作业不会留下任何痕迹。定期检查每个关键作业是否有一个最近的成功运行,可以同时捕获这两种情况:
SELECT j.jobname, max(d.end_time) AS last_success
FROM cron.job j
LEFT JOIN cron.job_run_details d
ON d.jobid = j.jobid AND d.status = 'succeeded'
GROUP BY j.jobname
HAVING max(d.end_time) < now() - interval '1 day'
OR max(d.end_time) IS NULL;此查询返回的任何作业都已经超过一天没有成功执行,需要进行检查。
结论
pg_cron 将 PostgreSQL 变成了自己的调度器。你可以在 SQL 中定义作业,数据库会执行它们,整个系统(定义、历史记录、失败情况)都可以通过普通查询进行查看。
在本教程中,你学习了该扩展如何在内部工作,如何在自己的服务器和托管服务上安装它,如何编写计划(包括 pg_cron 的秒级和月末扩展),以及如何将其应用于每个真实数据库都会积累的维护工作:修剪、汇总、刷新和真空。你还了解了如何监控作业、管理权限,以及识别何时真正的工作队列或编排工具会成为更好的工具。
如果你的基础设施目前有一个孤独的服务器,其唯一目的是从 crontab 中运行 psql,你现在知道如何将其退役。
感谢阅读!我撰写有关 PostgreSQL 和后端工程的内容。你可以在 LinkedIn 和 X 上与我联系。
一名对数据库和分布式系统有浓厚兴趣的后端工程师。
如果你读到这里,请感谢作者以表达你对他们的关心。说声谢谢
免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发人员。立即开始
ADVERTISEMENT