pg_cron是唯一支持sql内定时任务的方案,需预加载、创建扩展、授权用户;物化视图并发刷新须建btree唯一索引覆盖所有列;concurrently非增量,仍全量重算;任务注册后需检查worker状态与时区权限。

pg_cron 是唯一能“在 SQL 里写定时任务”的方案
PostgreSQL 本身不提供 CREATE SCHEDULED TASK 这类语法,所谓“SQL 定时任务”,实际只有 pg_cron 扩展支持——它把 cron 表达式和 SQL 命令封装进函数调用,任务元数据存在数据库内,重启不丢失。
其他方式(如系统 crontab + psql -c)本质是外部 shell 调度,不属于“SQL 内调度”。
必须满足的前提:
-
shared_preload_libraries = 'pg_cron'已写入postgresql.conf并重启服务 - 已执行
CREATE EXTENSION pg_cron - 调用
cron.schedule()的用户是超级用户,或已被显式授权(GRANT USAGE ON SCHEMA cron TO user_name)
CONCURRENTLY 刷新必须配唯一索引,否则直接报错
这是最常卡住的一步:哪怕你只写 REFRESH MATERIALIZED VIEW CONCURRENTLY mv_name,只要物化视图上没有满足条件的索引,就会立刻失败,报错信息明确:
ERROR: cannot refresh materialized view "mv_name" concurrently because it does not have a unique index
这个索引不是随便建一个就行,必须同时满足:
- 类型是
btree(不能是hash、gin等) - 定义为
UNIQUE(主键也行,但不能是PRIMARY KEY含隐式NOT NULL约束而列允许 NULL) - 覆盖物化视图所有列(即
CREATE UNIQUE INDEX ON mv_name (col1, col2, ...),顺序无关) - 不含表达式、函数、
NULLS NOT DISTINCT等扩展语法
例如,若视图定义是 SELECT date_trunc('day', ts), COUNT(*) FROM logs GROUP BY 1,那索引就得建在 (date_trunc) 上——但 date_trunc 是函数,不合法;得改用生成列或重写查询,让索引字段是普通列。
刷新命令必须全量重跑,别指望“自动增量”
REFRESH MATERIALIZED VIEW CONCURRENTLY 听起来像增量,其实不是。它只是在全量重算完新结果后,用唯一索引逐行比对旧数据,再做 INSERT/UPDATE/DELETE 同步——底层 SELECT 仍会完整执行一遍。
这意味着:
- 源表越大、聚合越复杂,刷新耗时越长,CPU 和 I/O 压力不减
- 即使只改了一行数据,整个物化视图定义的查询也会重跑
- 没有真正的基于 WAL 或时间戳的增量机制(PostgreSQL 15 仍不原生支持)
- 如果业务真需要分钟级新鲜度+低开销,得放弃物化视图,改用触发器维护汇总表或引入
pg_matview这类第三方扩展
任务注册后不会自动生效,得检查 pg_cron.job 状态
执行 SELECT cron.schedule(...) 只是插入一条记录到 cron.job 表,并不保证后台 worker 立即执行。常见疏漏点:
- worker 进程没起来:
SELECT * FROM cron.job_run_details WHERE jobid = (SELECT jobid FROM cron.job WHERE jobname = 'your_job');查最近运行记录,空结果说明根本没触发 - cron 后台进程被禁用:确认
pg_cron.enable_superuser_commands没被设为off(除非你主动关了) - 时区混淆:pg_cron 默认用数据库
timezone,不是系统时区;SHOW timezone;看当前值,避免误以为“每 5 分钟”其实是按 UTC 跑 - 权限不足:普通用户调用
cron.schedule报错permission denied for schema cron,不是语法错,是缺USAGE授权
真正可靠的验证方式,是等第一次预定时间过去后,手动查 mv_name 的 pg_class.reltuples 或加个时间戳列看数据是否更新——别只信 schedule 返回的 jobid。










