ON SCHEDULE 只有两种写法:AT 一次性、EVERY 周期,两者互斥。
不给 STARTS 时,周期事件的起点是「建事件的那一刻」——这往往不是你想要的。
跑完留不留由 ON COMPLETION 决定;补不补跑,复刻版和 MySQL 分歧最大。
这一篇讲 CREATE EVENT 的骨架:ON SCHEDULE 怎么写、STARTS/ENDS 怎么划范围、
ON COMPLETION 怎么定去留。全部在复刻版上真跑,MySQL 原码照贴。
和第 14 章的存储过程对照着看会更清楚:过程是「有人 CALL 才跑」,
事件是「到点自己跑」——差别只在「谁发起」。
所以这两者的「事件体」写法几乎是同一套东西:一段 SQL,或者 BEGIN ... END 里的多段。
一、两种时间表:AT 一次,EVERY 反复
ON SCHEDULE 后面只有两条路,教材各给了一个例子。
-- 一次性:到点跑一次就不管了
CREATE EVENT e_once
ON SCHEDULE AT '2026-11-01 02:00:00'
DO
INSERT INTO tb_log(note) VALUES ('凌晨清理');
-- 周期:每隔一段时间跑一次
CREATE EVENT e_every
ON SCHEDULE EVERY 1 DAY
STARTS '2026-11-01 03:00:00'
DO
DELETE FROM tb_stat WHERE id > 100;
AT 是一次性的,EVERY 是周期的;两者互斥,写一个就行。
在复刻版里,AT 对应「只给 at,不给 every」,EVERY 对应「给 every」。
内部就靠 every 是不是 None 来区分——None 就是一次性。
| 事件 | 周期 | 下次时间 |
|---|---|---|
| e_once | 一次性 | 11-01 02:00 |
| e_week | 604800 秒 | 11-08 00:00 |
AT 那条的下次时间是 02:00,EVERY 那条按「现在 + 7 天」算。
注意 EVERY 不给 STARTS 时,起点就是创建的那一刻——这往往不是你想要的。
想要「每天凌晨 3 点」,得显式写 STARTS,否则第一次是「建事件的时刻 + 1 天」。
这个坑很常见:下午三点建了个「每天一次」的事件,它从此每天下午三点跑,而你以为它会在凌晨跑。
顺带一句:EVERY 后面除了 1 DAY,还能写 1 HOUR、30 MINUTE、1 WEEK。
复刻版统一折成秒——604800 就是 7 天。
折成秒这件事本身没什么,但它提醒你:周期的粒度最终是「多久一次」,不是「看起来像什么」。
二、STARTS 与 ENDS:给周期事件划个范围
周期事件可以只在一个区间里跑:STARTS 说到点开始,ENDS 说到点为止。
教材的写法是 EVERY 1 HOUR STARTS '...' ENDS '...',复刻版把这两个字段存下来。
先建一个每小时一次、ENDS 在 03:00 的事件,然后把时钟直接推到 05:00:
| tick 到 | 跑了哪几次 |
|---|---|
| 01:00 | 01:00 |
| 02:00 | 02:00 |
| 03:00 | 03:00 |
| 05:00 | 没有(已过 ENDS) |
跑完 01:00、02:00、03:00 三次就停了,状态自己变成 DISABLE。
下次时间停在 04:00 没动,因为 04:00 那一刻它发现「已经过了 ENDS」,直接停用。
ENDS 不是「删掉」,是「不再跑」——事件还在,改一下 ENDS 还能接着用。
这个区别在运维上有用:临时停一个周期任务,改 ENDS 比 DISABLE 更能表达「跑到这儿为止」。
反过来,DISABLE 表达的是「现在停,以后再说」,两者语义不一样,别混着用。
混用的后果是交接时说不清:下一个人看到 DISABLE,不知道是「临时停」还是「到期了」。
三、ON COMPLETION:跑完删不删
一次性事件跑完之后,默认做法是「留着」,只把状态改成 DISABLE。
教材里讲 ON COMPLETION PRESERVE 和 NOT PRESERVE,说的就是这件事。
| 写法 | 跑完之后还剩什么 |
|---|---|
PRESERVE(默认) |
1 个,状态 DISABLE |
NOT PRESERVE |
0 个 |
第二行是 0 个——跑完自己把自己删了。
NOT PRESERVE 适合「只做一次」的活,比如一次性的数据修补。
但风险是:跑完之后连「我干过这件事」的记录都没有了,出问题查不到。
所以实践里更常见的是 PRESERVE 加手动清理,而不是让它自己消失。
这条和 17.4 的 DROP EVENT 是同一件事的两个入口:一个是你删它,一个是它自己走。
共同点是——删完之后,库里就没有「曾经有过这个任务」的证据了。
所以真正的做法是:任务本身可以走,记录要留下。
要么在事件体里先写一行日志,要么把这件事记在运维文档里。
四、补跑还是不补跑:复刻版和 MySQL 的分歧点
时钟一次跨过好几个周期时,中间那几个点要不要补?这一条两边不一样。
| 做法 | 一次推到 00:09,跑了几次 | 下次时间 |
|---|---|---|
| 补跑 | 9 次 | 11-01 00:10 |
| 不补跑 | 1 次 | 11-01 00:10 |
补跑跑了 9 次(1 到 9 分各一次),不补跑只跑 1 次、直接跳到 00:10。 注意两行的「下次时间」是一样的——差别只在「中间那些点补不补」。 MySQL 的事件调度器更接近右边这一列:它不承诺「错过的多次执行逐一补上」。 数据库停机两小时再起来,「每小时一次」的事件不会补跑两次,而是接着往下一个整点走。 这个差异在业务上很要命:如果一段逻辑假设「每个点都跑过」,停机之后数据就有空档。 所以用事件做什么都要加一道兜底:先查「上次跑到哪儿」,再决定补不补。 这比让调度器替你记住更可靠——它本来就没答应记住。 兜底的具体写法不复杂:建一张「任务进度表」,事件体开头读一行「上次处理到哪」,结尾写回一行。 这样无论漏了几个点,下一次都能从断点接上,跟调度器补不补没关系。
五、动手 Demo:AT、EVERY、ENDS、PRESERVE、补跑,五件事各验一遍
这一段按教材 17.2 的顺序走:先建一次性事件和周期事件,看下次时间怎么算;
再给周期事件加 ENDS,推到边界外看它怎么停;然后验 PRESERVE 和 NOT PRESERVE;
最后把「补跑」和「不补跑」放在一起对照。
import datetime
BASE = datetime.datetime(2026, 11, 1, 0, 0, 0)
s = Sched(BASE)
# ① 两种时间表
s.create("e_once", at=BASE + datetime.timedelta(hours=2))
s.create("e_week", every=7 * 86400) # 不给 STARTS:起点就是现在
for r in s.rows():
print(r) # e_once 11-01 02:00 / e_week 11-08 00:00
# ② ENDS:只在 01:00 到 03:00 之间跑
s.create("e_range", every=3600,
at=BASE + datetime.timedelta(hours=1),
ends=BASE + datetime.timedelta(hours=3))
print(s.tick(BASE + datetime.timedelta(hours=5))) # 三次
print(s.ev["e_range"]["status"], s.ev["e_range"]["runs"]) # DISABLE 3
# ③ ON COMPLETION:跑完留不留
for keep in (True, False):
a = Sched(BASE)
a.create("k", at=BASE, preserve=keep)
a.tick(BASE)
print(keep, len(a.ev)) # True -> 1,False -> 0
# ④ 补跑 vs 不补跑
for flag in (True, False):
b = Sched(BASE, catchup=flag)
b.create("e", every=60)
print(flag, len(b.tick(BASE + datetime.timedelta(minutes=9))))
实跑输出:
--------------------------------------------------------------------
创建事件:AT 一次,EVERY 反复
--------------------------------------------------------------------
两种时间表:AT 与 EVERY
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| -- 一次性:到点跑一次就不管了
| CREATE EVENT e_once
| ON SCHEDULE AT '2026-11-01 02:00:00'
| DO
| INSERT INTO tb_log(note) VALUES ('凌晨清理');
[教材原码 · 本沙箱不执行]
| -- 周期:每隔一段时间跑一次
| CREATE EVENT e_every
| ON SCHEDULE EVERY 1 DAY
| STARTS '2026-11-01 03:00:00'
| DO
| DELETE FROM tb_stat WHERE id > 100;
AT 是一次性的,EVERY 是周期的;两者互斥,写一个就行。
**在复刻版里,`AT` 对应「只给 `at`,不给 `every`」,`EVERY` 对应「给 `every`」。**
**内部就靠 `every` 是不是 `None` 来区分——`None` 就是一次性。**
事件 周期 状态 下次时间 已跑
--------------------------------------------
e_once 一次性 ENABLE 11-01 02:00 0
e_week 604800 秒 ENABLE 11-08 00:00 0
**`AT` 那条的下次时间是 02:00,`EVERY` 那条按「现在 + 7 天」算。**
**注意 `EVERY` 不给 `STARTS` 时,起点就是创建的那一刻——这往往不是你想要的。**
**想要「每天凌晨 3 点」,得显式写 `STARTS`,否则第一次是「建事件的时刻 + 1 天」。**
STARTS 与 ENDS:给周期事件划个范围
--------------------------------------------------------------------
事件 周期 状态 下次时间 已跑
-------------------------------------------
e_range 3600 秒 ENABLE 11-01 01:00 0
tick 到 05:00(越过 ENDS)-> e_range@11-01 01:00、e_range@11-01 02:00、e_range@11-01 03:00
事件 周期 状态 下次时间 已跑
--------------------------------------------
e_range 3600 秒 DISABLE 11-01 04:00 3
**跑完 01:00、02:00、03:00 三次就停了,状态自己变成 `DISABLE`。**
**`ENDS` 不是「删掉」,是「不再跑」——事件还在,改一下 `ENDS` 还能接着用。**
**这个区别在运维上有用:临时停一个周期任务,改 `ENDS` 比 `DISABLE` 更能表达「跑到这儿为止」。**
ON COMPLETION:跑完删不删
--------------------------------------------------------------------
**一次性事件跑完之后,默认做法是「留着」,只把状态改成 `DISABLE`。**
**教材里讲 `ON COMPLETION PRESERVE` 和 `NOT PRESERVE`,说的就是这件事。**
写法 跑完之后还剩什么
----------------------------------
`PRESERVE` 1 个,状态 DISABLE
`NOT PRESERVE` 0 个
**第二行是 0 个——跑完自己把自己删了。**
**`NOT PRESERVE` 适合「只做一次」的活,比如一次性的数据修补。**
**但风险是:跑完之后连「我干过这件事」的记录都没有了,出问题查不到。**
**所以实践里更常见的是 `PRESERVE` 加手动清理,而不是让它自己消失。**
补跑还是不补跑:这是复刻版和 MySQL 的分歧点
--------------------------------------------------------------------
**时钟一次跨过好几个周期时,中间那几个点要不要补?这一条两边不一样。**
做法 一次推到 00:09,跑了几次 下次时间
---------------------------------------------
补跑 9 次 11-01 00:10
不补跑 1 次 11-01 00:10
**补跑跑了 9 次(1 到 9 分各一次),不补跑只跑 1 次、直接跳到 00:10。**
**MySQL 的事件调度器更接近右边这一列:它不承诺「错过的多次执行逐一补上」。**
**数据库停机两小时再起来,「每小时一次」的事件不会补跑两次,而是接着往下一个整点走。**
**这个差异在业务上很要命:如果一段逻辑假设「每个点都跑过」,停机之后数据就有空档。**
**所以用事件做什么都要加一道兜底:先查「上次跑到哪儿」,再决定补不补。**
**这比让调度器替你记住更可靠——它本来就没答应记住。**
第一组的两行下次时间值得记:AT 是「你给的那个时刻」,EVERY 是「现在加一个周期」。
所以周期事件千万别省 STARTS,除非你确实想「从现在开始算」。
第二组是 ENDS 的效果:跑满三次、状态自己变 DISABLE、下次时间停在 04:00。
第三行那个 04:00 容易被误读成「它想在 04:00 再跑」——其实不是,
那是「上一次算出来的下次时间」,只是被 ENDS 挡住了没执行。
第四组两个数(1 和 0)是 ON COMPLETION 的全部差别。
0 那一行意味着连「曾经有过这个事件」都查不到了,这是它唯一的代价,也是最容易被低估的代价。
30 秒实验
- 把
e_week的every从 7 天改成 1 天、再给一个 03:00 的at, 看下次时间是「明天 03:00」还是「现在 + 1 天」。 - 把
ENDS设在两次执行之间(比如 02:30),看它是「跑完 02:00 就停」还是「02:00、03:00 都跑」。 - 把
catchup设成False,让时钟一次跨过 9 个周期,再把tick循环跑两遍, 看第二遍还会不会补。
常见误区
- 以为
EVERY 1 DAY就是「每天零点」。不给STARTS时起点是建事件的时刻,可能是下午三点。 - 以为
ENDS会把事件删掉。它只是让事件停用,事件本身还在,改一下ENDS就能接着跑。 - 以为停机期间错过的事件会在恢复后逐一补上。MySQL 不承诺补跑,错过的点就那么过去了; 要补得自己在事件体里查断点。
自测三问(点开对答案)
1. `AT` 和 `EVERY` 能一起写吗? —— 不能,两者互斥。 `AT` 表示「就这一刻跑一次」,`EVERY` 表示「每隔一段跑一次」,语义上就不可能同时成立。 要「从某刻起周期跑」,用 `EVERY ... STARTS ...`。 2. 周期事件不给 `STARTS`,第一次什么时候跑? —— 创建时刻加一个周期。比如下午三点建「每天一次」,第一次是明天下午三点,不是明天零点。 想要固定时刻必须显式写 `STARTS`。 3. `ON COMPLETION NOT PRESERVE` 的风险是什么? —— 跑完之后事件自己消失,库里再也查不到「曾经有过这个任务」。 出问题想复盘时,连它存在过的证据都没有。创建讲完了。下一篇讲修改:改状态、改周期、改事件体,以及为什么「改名」只能重建。
- ☐ (本篇完,下一篇 17.3 修改事件)