Every Quartz.NET release that changed the database schema, in order, with the migration to run and what happens if you skip it.
Quartz.NET never migrates your schema. A 4.x store asked to provision its own creates a missing schema, but cannot move an existing one forward: a guarded CREATE TABLE skips a table that exists without looking inside, so a table missing a column stays missing it. Read from your current version down to your target version and run what each section lists.
Warning
Always run migration scripts in a test environment against a copy of your production database first.
How to use this page
Scripts live in database/migrations/, one folder per version. In each folder, run the file whose suffix matches your database: _sqlServer, _postgres, _mysql_innodb, _oracle, _sqlite, _firebird. Each file runs as is, with no editing.
- Apply every version between where you are and where you are going, in ascending order, and skip none. Some migrations are optional (you can defer them), but they are cumulative, not alternatives. Skipping 3.17 and later running 3.19 leaves you without the 3.17 column; nothing goes back and adds it.
- Re-running is safe. Every script except the SQL Server 1.0→2.0 one checks before it acts, so re-running a migration is a no-op and a half-applied migration can be re-run. The exception is SQLite
ADD COLUMN: SQLite has no conditional DDL, so those statements fail on a second run. - A fresh install needs none of this. The scripts in
database/tables/already create everything below.
What do I need?
| Coming from | Going to | What to run |
|---|---|---|
| 1.x | any 2.x/3.x | 2.0, 2.2, 2.6, then 3.x as below |
| 2.0 / 2.1 | 3.x | 2.2, 2.6, 3.0, then the optional 3.x ones |
| 2.2–2.5 | 3.x | 2.6, 3.0, then the optional 3.x ones |
| 2.6 | 3.x | 3.0, then the optional 3.x ones |
| 3.0–3.16 | latest 3.x | 3.17, 3.18, 3.19, 3.20 — all optional |
| any 3.x | 4.0 / 4.1 | 4.0 — mandatory, and it folds in everything from 3.17 onward |
| any 3.x | 4.2 | 4.0, then 4.2 — both mandatory |
| any 3.x | 4.3+ | 4.0, 4.2, then 4.3 — all mandatory |
| 4.0 / 4.1 | 4.2 | 4.2 — mandatory |
| 4.0 / 4.1 | 4.3+ | 4.2, then 4.3 — both mandatory |
| 4.2 | 4.3+ | 4.3 — mandatory; then 4.6 for 4.6+ |
| 4.3 | 4.4 / 4.5 | Nothing mandatory. 4.4 only with UseExecutionHistory() |
| 4.3 / 4.4 / 4.5 | 4.6+ | 4.6: the version, interrupt and misfire threshold columns mandatory, the history indexes optional; 4.4 only with UseExecutionHistory(); then 4.7 for 4.7+ |
| 4.6 | 4.7+ | 4.7: the fixed-delay column, mandatory |
Upgrading to 4.x is mandatory
This is the one migration you cannot defer.
- 3.x probes for
MISFIRE_ORIG_FIRE_TIME,EXECUTION_GROUP,PREFERRED_NODEandPREFERRED_NODE_AUTOwhen the scheduler starts. If a column is missing, it logs a warning and turns the feature off. That is why those migrations are optional on 3.x. - 4.x removed those probes and assumes all four columns exist. It also needs two columns 3.x never had,
RETRY_POLICYandRETRY_ATTEMPTonQRTZ_TRIGGERS, and a table 3.x never had,QRTZ_PAUSED_JOB_GRPS.
So even a 3.x database with every optional migration applied does not work with 4.x until 4.0 has been applied.
At startup, 4.x checks that every table it needs is queryable, and runs one SELECT <column> … WHERE 1 = 0 per column this migration adds to a table 3.x already had. A database missing the table or any of those columns is refused, and the message names the column and the script. The check cannot see a column's type or width, so run the whole script.
Version 2.0
Required when upgrading from 1.x. Nothing later applies until this has run.
The 1.x → 2.x schema overhaul:
the listener tables are dropped;
varchar(1)flag columns become realbitcolumns;IS_STATEFULis replaced byIS_NONCONCURRENTandIS_UPDATE_DATA;SCHED_NAMEis added to every table, with the primary keys and indexes rebuilt around it.Script:
migrations/2.0/schema_10_to_20_upgrade_sqlServer.sqlSQL Server only. It is a sample, and needs adapting for other databases.
Not idempotent. It drops and recreates objects unconditionally, so it fails on a partially-migrated database. Run it once, on a restorable copy.
Warning
The script defaults SCHED_NAME to TestScheduler. If you have existing data, change it to match your quartz.scheduler.instanceName.
Version 2.2
Required when upgrading from 2.0 or 2.1.
Adds SCHED_TIME to QRTZ_FIRED_TRIGGERS, so recovery jobs can see both the scheduled and the actual fire time (#113).
- Scripts:
migrations/2.2/— all databases
The column is NOT NULL with no default, so the ALTER fails on a table that holds rows. QRTZ_FIRED_TRIGGERS only holds in-flight entries, so stop the scheduler and clear it first:
DELETE FROM QRTZ_FIRED_TRIGGERS;
Version 2.6
Required when upgrading from 2.5 or earlier.
Adds TIME_ZONE_ID to QRTZ_SIMPROP_TRIGGERS and QRTZ_CRON_TRIGGERS, so a trigger's time zone survives a restart (#136).
- Scripts:
migrations/2.6/— all databases
Tips
Older copies of this migration only altered QRTZ_SIMPROP_TRIGGERS. QRTZ_CRON_TRIGGERS needs the column too (#1985). If you upgraded 2.5→2.6 some time ago, check that both tables have it.
Version 3.0
Required when upgrading a SQL Server database from 2.6.
Converts the deprecated IMAGE columns to VARBINARY(MAX) (#291): QRTZ_CALENDARS.CALENDAR, QRTZ_JOB_DETAILS.JOB_DATA, QRTZ_BLOB_TRIGGERS.BLOB_DATA and QRTZ_TRIGGERS.JOB_DATA.
- Script:
migrations/3.0/schema_26_to_30_upgrade_sqlServer.sql - SQL Server only; no other dialect used
IMAGE.
Version 3.17
Optional on 3.x. Required on 4.x.
Adds MISFIRE_ORIG_FIRE_TIME to QRTZ_TRIGGERS (#2899). It stores the original scheduled fire time before misfire handling overwrites it.
- Scripts:
migrations/3.17/— all databases
If you skip it: AdoJobStore keeps working, but for misfired triggers under the "fire now" misfire policies ScheduledFireTimeUtc equals FireTimeUtc, instead of the time the trigger was supposed to fire. RAMJobStore is unaffected.
-- SQL Server
ALTER TABLE QRTZ_TRIGGERS ADD MISFIRE_ORIG_FIRE_TIME bigint NULL;
Version 3.18
Optional on 3.x. Required on 4.x.
Adds EXECUTION_GROUP to QRTZ_TRIGGERS and QRTZ_FIRED_TRIGGERS (#3004). It carries the execution group tag that per-node thread limits are enforced against. Both tables need it.
- Scripts:
migrations/3.18/— all databases
If you skip it: execution groups still work, but the limit is applied by in-memory filtering after acquisition rather than in the acquire query, so a node acquires triggers it then has to put back.
-- SQL Server
ALTER TABLE QRTZ_TRIGGERS ADD EXECUTION_GROUP nvarchar(200) NULL;
ALTER TABLE QRTZ_FIRED_TRIGGERS ADD EXECUTION_GROUP nvarchar(200) NULL;
Version 3.19
Optional on 3.x. Required on 4.x.
Adds PREFERRED_NODE and PREFERRED_NODE_AUTO to QRTZ_TRIGGERS (#3013, #3144), which back node affinity.
- Scripts:
migrations/3.19/— all databases
Add both columns together. Quartz enables node affinity only when both are present.
If you skip it: node affinity is unavailable. The scheduler logs a warning at startup and otherwise behaves exactly as in 3.18.
-- SQL Server
ALTER TABLE QRTZ_TRIGGERS ADD PREFERRED_NODE nvarchar(200) NULL;
ALTER TABLE QRTZ_TRIGGERS ADD PREFERRED_NODE_AUTO bit NOT NULL DEFAULT 0;
Version 3.20
Optional, performance only.
Realigns the index set with the statements AdoJobStore issues (#3203).
Scripts:
migrations/3.20/— all databasesEvery Quartz statement filters
SCHED_NAMEfirst, but several shipped indexes did not lead with it and could not serve a single-scheduler lookup. PostgreSQL was worst: 9 of 11 indexes were affected, andIDX_QRTZ_T_NFT_SThad its columns in the wrong order.Indexes that are a leftmost prefix of a wider one, or that no statement can drive a scan from, are dropped.
SQLite previously had no secondary indexes, so every acquire poll was a full table scan; this adds them.
If you skip it: everything works, with more index maintenance on writes and worse plans on reads. A database created from the current tables/ script already matches.
Tips
On a busy PostgreSQL database use CREATE INDEX CONCURRENTLY / DROP INDEX CONCURRENTLY. Neither can run inside a transaction block, so run those statements one at a time.
Version 4.0
Mandatory. See above.
- Scripts:
migrations/4.0/— all databases
There are two files, run at different moments:
| File | Status | When to run it |
|---|---|---|
schema_30_to_40_upgrade_<db>.sql | Mandatory | Now. Everything in it is safe while 3.x nodes are still up. |
schema_30_to_40_indexes_<db>.sql | Optional, performance only | Once the last 3.x node has shut down, or straight afterwards on an offline upgrade. |
The upgrade file
It applies everything from 3.17, 3.18 and 3.19, plus the retry columns and the QRTZ_PAUSED_JOB_GRPS table. Run it whether or not you applied the optional migrations; every statement is guarded, so it is safe on a partially-migrated database.
| # | Change | Status |
|---|---|---|
| 1 | MISFIRE_ORIG_FIRE_TIME column | required |
| 2 | EXECUTION_GROUP columns | required |
| 3 | PREFERRED_NODE / PREFERRED_NODE_AUTO columns | required |
| 4 | RETRY_POLICY / RETRY_ATTEMPT columns | required |
| 5 | QRTZ_PAUSED_JOB_GRPS table | required |
Sections 1–3 fold in their 3.x counterparts. Sections 4 and 5 are new in 4.x, and are why this migration is required even for a database fully migrated on 3.x.
RETRY_POLICYandRETRY_ATTEMPTonQRTZ_TRIGGERSback a trigger's retry policy (#3520): the policy in its stored string form, and how many retries of the executing occurrence have been made. Both are nullable with no default, so existing rows read as "no retry policy" and nothing needs backfilling.QRTZ_PAUSED_JOB_GRPSholds one row per paused job group, mirroringQRTZ_PAUSED_TRIGGER_GRPS(#3336). 3.x pauses a job group without recording it, soIsJobGroupPausedansweredfalsefor every group and the pause was lost on restart. 4.x records the group names, soJobGroup.Pausedis accurate andQueryJobGroups(new JobGroupQuery { Paused = true })lists them. A group with no jobs can be paused.
From an earlier 4.0 preview: run both files again. Every statement is guarded, so the second run only applies what the preview lacked: the retry columns, this table on a preview old enough to predate it, and the index set as a pre-release moved it. On SQLite the upgrade file's sections 1–4 are not guarded (it has no conditional ADD COLUMN), so check PRAGMA table_info(QRTZ_TRIGGERS) and run only the sections whose columns are missing. The index file is guarded on SQLite too.
The index file
It supersedes 3.20 and lands the 4.x index shape. It is separate because it cannot run while 3.x nodes are up: it drops IDX_QRTZ_T_NFT_ST_MISFIRE, which 3.x drives its misfire sweep from and 4.x does not read (#3656).
- Run it top to bottom: the drops assume the creates above them succeeded. Re-run it as often as you like; every statement is guarded.
- A 4.x node starts fine against a database with the upgrade file but not this one; it scans where it would otherwise seek.
What it changes:
- Adds
IDX_QRTZ_J_G_NandIDX_QRTZ_T_G_N. The 4.x listing queries page withORDER BY JOB_GROUP, JOB_NAMEandORDER BY TRIGGER_GROUP, TRIGGER_NAME, and the primary keys are name-before-group. Without these indexes each page is a scan plus a sort. - Reshapes
IDX_QRTZ_T_NFT_ST, the acquisition index, from(SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME)to(SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME ASC, PRIORITY DESC, MISFIRE_INSTR)(#3510).- Acquisition orders by
NEXT_FIRE_TIME ASC, PRIORITY DESC; an index with matching directions lets the engine take the first entry instead of reading and sorting every candidate. On SQL Server, a round trip against 5,000 due triggers goes from 21.6 ms and 20,395 logical reads to 0.6 ms and 8. MISFIRE_INSTRlets a backlog of misfired triggers below the acquisition window be skipped inside the index rather than one table lookup at a time.- The name is unchanged but the columns are not, so the script drops the index before recreating it; a guarded
CREATE INDEXwould find the name taken and keep the old shape. - Firebird keeps the three-column index. Its indexes take one direction for the whole index, so it cannot express this one.
- Acquisition orders by
- Drops
IDX_QRTZ_T_NFT_ST_MISFIRE, which SQL Server, MySQL, Oracle and Firebird created over(SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE)and PostgreSQL and SQLite never did (#3656).- It leads with
MISFIRE_INSTR, which both misfire statements compare with<> -1, and no B-tree can seek past that. Those statements filterSCHED_NAMEandTRIGGER_STATEby equality andNEXT_FIRE_TIMEby range: the reshaped acquisition index's leading columns. - Measured plan by plan on all four engines that had it, no optimizer picks it for either statement (#3608, #3656). MySQL only appeared to, because
MySQLDelegatenamed it in aFORCE INDEXhint; the hint now names the acquisition index. - The drop comes after the reshape in the same file on purpose, so a schema that never took the reshape takes it first and is never left with neither index.
- It leads with
Node affinity needs no data migration: 3.x and 4.x store pins identically.
SQLite trigger names
database/tables/tables_sqlite.sql now names its four referential-integrity triggers QRTZ_DELETE_SIMPLE_TRIGGER, QRTZ_DELETE_SIMPROP_TRIGGER, QRTZ_DELETE_CRON_TRIGGER and QRTZ_DELETE_BLOB_TRIGGER. They were DELETE_SIMPLE_TRIGGER and so on, with no table prefix, so two Quartz schemas could not share one SQLite database whatever their table prefixes: the second CREATE TRIGGER collided with the first.
No action is required on an existing database. Nothing in Quartz names these triggers; SQLite runs them itself when a row leaves QRTZ_TRIGGERS, so an older schema keeps working under the old names. Rename them only to put a second Quartz schema in the same file:
DROP TRIGGER DELETE_SIMPLE_TRIGGER;
DROP TRIGGER DELETE_SIMPROP_TRIGGER;
DROP TRIGGER DELETE_CRON_TRIGGER;
DROP TRIGGER DELETE_BLOB_TRIGGER;
then re-create them from the current tables_sqlite.sql, replacing QRTZ_ with your table prefix. There is no migration script for this; it changes nothing Quartz stores or reads.
Tips
The 4.0 scripts live on main only. The 3.x branch links to them rather than carrying a copy, because this upgrade is decided by 4.x's schema and a copy would go stale. The links above are the right ones.
Version 4.2
Two scripts; only the first is mandatory.
migrations/4.2/add_continuations_<db>.sql— mandatory for 4.2 and later, all databasesmigrations/4.2/add_execution_history_<db>.sql— optional, needed only by a store configured withUseExecutionHistory()
The continuation columns
Mandatory for 4.2 and later. The first schema change since 4.0.
Three nullable columns on QRTZ_TRIGGERS carry a conditional continuation: a trigger that waits, in the store, for another trigger's firing to end (#3805).
| Column | What it holds |
|---|---|
CONTINUES_TRIGGER_NAME | The name of the trigger whose firing this one waits for |
CONTINUES_TRIGGER_GROUP | That trigger's group |
CONTINUATION_CONDITION | The outcomes that release the wait, as flags (below) |
CONTINUATION_CONDITION flags: 1 on success, 2 on failure, 4 on cancellation, 8 on veto, 15 however it ends. A row that names a parent but has no condition reads as 15.
- The waiting trigger is in a new
TRIGGER_STATE,AWAITING, and is never acquired while there. - The parent's completion resolves it inside the parent's own lock and transaction. An outcome the condition names releases the trigger into the ordinary schedule, with its next fire time the later of "now" and its own
START_TIME. Any other outcome deletes it. - The node that ran the parent resolves it, so a continuation survives the failure of the node that scheduled it.
- All three columns are nullable with no default, so existing rows are valid at once and nothing needs backfilling.
- No index is added. The lookup is
SCHED_NAME = ? AND TRIGGER_STATE = 'AWAITING' AND CONTINUES_TRIGGER_NAME = ? AND CONTINUES_TRIGGER_GROUP = ?; its two leading equality columns are whatIDX_QRTZ_T_NFT_STalready leads with, andAWAITINGis a small partition even in a schedule that uses continuations heavily. Measure before adding one.
Rolling 4.1 → 4.2
Run the migration while 4.1 nodes are still up. A 4.1 node's trigger INSERT names its own columns, its acquisition and misfire sweeps select WAITING, and its cluster recovery touches ACQUIRED and BLOCKED, so nothing it does on its own picks up an AWAITING row. But it reads an unknown state string as waiting:
- it reports an
AWAITINGrow asNormal; - its single-trigger
PauseTriggerwritesPAUSEDover one, which a resume then releases without its parent; - its reschedule rewrites one as an ordinary trigger.
A 4.1 node cannot resolve a continuation at all: a parent completing there leaves the triggers waiting on that firing where they are. Since 4.3, a node's misfire pass settles a continuation whose parent trigger no longer exists — a spent one-off that completed on a 4.1 node — as a deleted parent is settled: OnAnyOutcome is released, any other condition is parked in ERROR. A repeating parent's continuations still wait for its next completion on a 4.2+ node. So while any 4.1 node runs, do not pause, resume or reschedule a continuation from it, and:
- Run
add_continuations_<db>.sql. - Roll every node to 4.2.
- Then start scheduling continuations.
A 4.2 node refuses to start against a database without this migration, because it names all three columns in the statement that stores every trigger. The startup check says which column and which script.
ProvisionSchema() does not help here: it creates missing tables and never adds a column to an existing table. Run the script.
The execution history tables
Optional. add_execution_history_<db>.sql creates two tables that only UsePersistentStore(store => store.UseExecutionHistory()) reads or writes (#3771):
| Table | One row per |
|---|---|
QRTZ_EXECUTION_HISTORY | Finished execution |
QRTZ_MISFIRE_HISTORY | Missed firing |
QRTZ_EXECUTION_HISTORYholds the node that ran it, the job and trigger, when it fired, how long it took in ticks, whether it threw, what it said, which attempt at the occurrence it was (RETRY_ATTEMPT) and whether the trigger answered it with another (RETRY_SCHEDULED).QRTZ_MISFIRE_HISTORYholds the trigger, the node that noticed, when it noticed, and the firing that was missed. Nothing ran, so there is no duration or outcome; that is why it is a separate table.- Both are keyed by
(SCHED_NAME, ENTRY_ID). The store writesENTRY_IDexactly as it writesQRTZ_FIRED_TRIGGERS.ENTRY_ID, because no two dialects spell an identity column the same way, and nothing reads the number. - Neither table has a foreign key, since a history row outlives the trigger and job it names. No other statement in the schema mentions them.
- Each has two indexes:
(SCHED_NAME, <time>)for the age query and the retention sweep, and(SCHED_NAME, INSTANCE_NAME)for the dashboard's node filter. - A search by job or trigger name is a scan, deliberately: it lowercases the key to match the in-memory history, which no index can serve, and the feed it searches is bounded by the retention window.
RETRY_ATTEMPTandRETRY_SCHEDULEDare not indexed either: the "failed after retries" filter (SUCCEEDED = 0 AND RETRY_SCHEDULED = 0) runs over the same bounded feed.
When you need it:
- A scheduler that keeps no history never probes for these tables, so a database created by 4.0 or 4.1 keeps working untouched.
- A store configured for history refuses to start without them, naming this script.
- A fresh install from
database/tables/creates them either way, and so doesProvisionSchema(). This is the one 4.2 script provisioning can replace, because what is missing is whole tables, not columns of an existing one.
It is safe in a mixed cluster in both directions. A 4.0 or 4.1 node cannot see these tables, and a 4.2 node that does not ask for history neither writes nor reads them. The nodes that do ask share one history, and every row carries the instance id that produced it.
The store trims both tables itself, to ExecutionHistoryOptions.Retention (24 hours by default) and MaxEntriesPerScheduler (2,000); see the persistent store's execution history.
SQL Server memory-optimized and pre-2016 schemas
Required for a schema created from tables_sqlServerMOT.sql or tables_sqlServer_Below2016.sql under 4.2.0, when the store keeps execution history. Fixed in 4.2.1 and 4.3.
- Under 4.2.0, those two scripts created
QRTZ_EXECUTION_HISTORYwithoutRETRY_ATTEMPTandRETRY_SCHEDULED. - With
UseExecutionHistory()on, every history write fails withInvalid column name 'RETRY_ATTEMPT'. - A schema from
tables_sqlServer.sql,add_execution_history_sqlServer.sqlorProvisionSchema()already has both columns.
Add the two columns. Each statement is guarded, so re-running it is safe:
IF COL_LENGTH('dbo.QRTZ_EXECUTION_HISTORY', 'RETRY_ATTEMPT') IS NULL
ALTER TABLE [dbo].[QRTZ_EXECUTION_HISTORY] ADD [RETRY_ATTEMPT] int NOT NULL DEFAULT 0;
IF COL_LENGTH('dbo.QRTZ_EXECUTION_HISTORY', 'RETRY_SCHEDULED') IS NULL
ALTER TABLE [dbo].[QRTZ_EXECUTION_HISTORY] ADD [RETRY_SCHEDULED] bit NOT NULL DEFAULT 0;
Version 4.3
Five scripts; the first three are mandatory (#3874, #3875, #3879).
| Script | Status | Adds |
|---|---|---|
migrations/4.3/add_fire_progress_<db>.sql | Mandatory for 4.3 and later | PROGRESS, PROGRESS_MESSAGE on QRTZ_FIRED_TRIGGERS |
migrations/4.3/add_overlap_policy_<db>.sql | Mandatory for 4.3 and later | OVERLAP_POLICY on QRTZ_TRIGGERS |
migrations/4.3/add_pause_reason_<db>.sql | Mandatory for 4.3 and later | PAUSE_REASON, PAUSED_BY, PAUSED_AT on QRTZ_TRIGGERS, QRTZ_PAUSED_TRIGGER_GRPS and QRTZ_PAUSED_JOB_GRPS |
migrations/4.3/add_execution_log_<db>.sql | Optional: only with UseExecutionHistory() | EXECUTION_LOG on QRTZ_EXECUTION_HISTORY |
migrations/4.3/add_misfire_reason_<db>.sql | Optional: only with UseExecutionHistory() | REASON on QRTZ_MISFIRE_HISTORY |
The progress columns
| Column | What it holds |
|---|---|
PROGRESS | The percentage, 0 to 100, a running job last passed to ReportProgress |
PROGRESS_MESSAGE | The message it passed with it, truncated to 250 characters |
- Written at most once a second per firing, and only when the value changes, by
ENTRY_ID. The row is the writing node's own, so no lock is taken. - Oracle declares
PROGRESS_MESSAGEasVARCHAR2(1000):VARCHAR2counts bytes, and 250 characters of UTF-8 can take 1,000. - Firebird declares it
VARCHAR(250), which is 250 bytes in a database without a character set. The store cuts the message to 250 bytes there; see text columns that count bytes. - Read by the fire-instance listing, which is what the dashboard's Currently Executing page shows cluster-wide.
- No index: the write is by primary key.
- A 4.3 node refuses to start without them; the startup check names the column and the script.
The execution log column
EXECUTION_LOG holds the log lines an execution wrote, captured by UseExecutionLogCapture(). A large object on every dialect: nvarchar(max), TEXT, LONGTEXT, CLOB, BLOB SUB_TYPE TEXT.
- The history listing never selects it; only the read of one entry does.
- A store configured with
UseExecutionHistory()refuses to start without it. No other store probes for it. - Run it only on a database that has
QRTZ_EXECUTION_HISTORY: it alters that table, and fails where the table is missing. A database without the table runs4.2/add_execution_history_<db>.sqlfirst.
The overlap policy column
OVERLAP_POLICY is the integer of the trigger's OverlapPolicy.
| Value | Policy |
|---|---|
NULL, 0 | Default |
1 | Skip |
2 | BufferOne |
3 | CancelPrevious |
4 | AllowAll |
19 | CancelPrevious, set while a firing that started under another policy may still run; the store clears the 16 once none does |
- Read and written with the rest of the trigger's row. No index.
- A 4.3 node refuses to start without it; the startup check names the column and the script.
- What each policy does: Overlap Policy.
The misfire reason column
REASON says why a row of QRTZ_MISFIRE_HISTORY is there.
| Value | Reason |
|---|---|
NULL, 0 | Missed: a misfire; the trigger's misfire instruction was applied |
1 | Overlap: OverlapPolicy.Skip dropped the firing because the previous one was still running |
- A store configured with
UseExecutionHistory()refuses to start without it. No other store probes for it. - Run it only on a database that has
QRTZ_MISFIRE_HISTORY, as for the execution log column.
The pause columns
The same three columns on QRTZ_TRIGGERS and on both paused-group tables.
| Column | What it holds |
|---|---|
PAUSE_REASON | Why it was paused, cut to 250 characters |
PAUSED_BY | Who asked, cut to 200 characters; quartz:retries-exhausted when the scheduler paused a trigger whose retries ran out |
PAUSED_AT | When, in ticks |
- A 4.3 node writes all three for a pause that gives a reason or a requester. A pause that gives neither runs 4.2's statements and leaves them
NULL. - A 4.3 node clears a trigger's on its own resume. A group's row is deleted on resume.
- Read only while the trigger is
PAUSEDorPAUSED_BLOCKED, or while the group's row exists. - Oracle declares
PAUSE_REASONasVARCHAR2(1000)andPAUSED_BYasVARCHAR2(800), for the reasonPROGRESS_MESSAGEis wider. Firebird declares themVARCHAR(1000)andVARCHAR(800): in a database created without a default character set, itsVARCHARcounts bytes too. - No index. A 4.3 node refuses to start without them; the startup check names the column and the script.
- What a pause records, and how to read it: Pausing with a Reason.
Rolling 4.2 → 4.3
Run the scripts while 4.2 nodes are still up. Every column is nullable with no default, and a 4.2 node never names them: its firings read as having reported no progress, its history rows carry no log, and its misfire rows read as misfires.
Roll every node before giving a trigger an overlap policy. A 4.2 node ignores OVERLAP_POLICY and fires the trigger as Default does, overlapping.
Roll every node before relying on a pause reason. A 4.2 node's pause leaves the columns NULL, which reads as no reason. Its resume leaves a trigger's columns behind. A 4.3 node reads them only while the trigger is paused, so a later pause of it without a reason, on either version, reports the old reason.
A fresh install from database/tables/, and ProvisionSchema(), create all fourteen columns.
Version 4.4
One script, optional: only a store configured with UseExecutionHistory() needs it (#3958).
| Script | Status | Adds |
|---|---|---|
migrations/4.4/add_execution_outcome_<db>.sql | Optional: only with UseExecutionHistory() | RESULT, SUMMARY, METRICS, MANUAL, FIRE_INSTANCE_ID, JOB_INPUT, JOB_INPUT_TOO_LARGE on QRTZ_EXECUTION_HISTORY; IDX_QRTZ_EH_JOB_TIME; the QRTZ_JOB_STATUS table |
- A store configured with
UseExecutionHistory()refuses to start without them; the startup check names the table or column and the script. No other store probes for them. - Run it only on a database that has
QRTZ_EXECUTION_HISTORY: its first statement alters that table. A history 4.2 created needs4.3/add_execution_log_<db>.sqlandadd_misfire_reason_<db>.sqlfirst.
The outcome columns
What a run reported through context.Result, on QRTZ_EXECUTION_HISTORY.
| Column | What it holds | Declared as |
|---|---|---|
RESULT | The integer of the run's JobRunResult | REASON on QRTZ_MISFIRE_HISTORY |
SUMMARY | One line, cut to 1,000 characters | ERROR_MESSAGE |
METRICS | The reported values, as JSON | EXECUTION_LOG |
MANUAL | Whether TriggerJob fired it | SUCCEEDED, nullable |
FIRE_INSTANCE_ID | The firing's id, linking the row to its span and log scope | QRTZ_FIRED_TRIGGERS.ENTRY_ID |
- All five are nullable with no default. A row a 4.3 node wrote leaves them
NULL, and its outcome is read fromSUCCEEDED. FIRE_INSTANCE_IDis neither unique nor indexed. A fire instance id is not durable across a restart, and the history write is never retried.
The input columns
What the run was given, on QRTZ_EXECUTION_HISTORY, written only with ExecutionHistoryOptions.RecordInput (#4012).
| Column | What it holds | Declared as |
|---|---|---|
JOB_INPUT | The run's input, the string stored under QRTZ_JOB_INPUT | EXECUTION_LOG |
JOB_INPUT_TOO_LARGE | Whether the input was over MaxInputBytes, and so not kept | SUCCEEDED, nullable |
- Both are nullable with no default.
JOB_INPUTisNULLwhen nothing was recorded. - The listing never reads
JOB_INPUT; the single-row read does.
The job index
IDX_QRTZ_EH_JOB_TIME is (SCHED_NAME, JOB_GROUP, JOB_NAME, FIRED_TIME): one job's history by time. The table is bounded by ExecutionHistoryOptions, so the index builds in moments.
The job status table
QRTZ_JOB_STATUS holds one row per job, keyed by (SCHED_NAME, JOB_GROUP, JOB_NAME). The store writes it with each history row, so it outlives the history's retention.
| Column | What it holds |
|---|---|
FIRST_FIRED_TIME, LAST_FIRED_TIME | When the job first fired, and when its last run fired |
LAST_RESULT, LAST_SUMMARY | The last run's RESULT and SUMMARY |
LAST_RUN_TIME | How long the last run took, in ticks |
LAST_INSTANCE_NAME, LAST_ENTRY_ID | The node that ran it, and its history row |
LAST_SUCCESS_TIME, LAST_FAILURE_TIME | When a run last succeeded, and last failed |
LAST_FAILURE_MESSAGE | What the last failure said |
CONSECUTIVE_FAILURES, RUN_COUNT, FAILURE_COUNT | Counters, NOT NULL DEFAULT 0 |
NOT NULL: the key,FIRST_FIRED_TIME,LAST_FIRED_TIME,LAST_RESULT,LAST_RUN_TIME,LAST_INSTANCE_NAMEand the counters.- No foreign key: a 4.3 node deleting a job must not trip on a row it has never heard of.
Byte widths
SUMMARY, LAST_SUMMARY and LAST_FAILURE_MESSAGE are declared as ERROR_MESSAGE is: VARCHAR2(4000) on Oracle and VARCHAR(1000) on Firebird. See text columns that count bytes.
Rolling 4.3 → 4.4
Run the script while 4.3 nodes are still up. Every column is nullable with no default, and a 4.3 node never names them: its history rows carry no outcome. A 4.3 node never touches QRTZ_JOB_STATUS, so the rollup counts only the runs 4.4 nodes record until every node is 4.4.
A fresh install from database/tables/, and ProvisionSchema(), create all of it.
SQL Server: one foreign key fewer
ProvisionSchema() no longer creates FK_QRTZ_BLOB_TRIGGERS_QRTZ_TRIGGERS, which tables_sqlServer.sql never created (#3949). There is no script: a database that has the key keeps it, and nothing depends on it.
Version 4.6
Four scripts: three mandatory (#4067, #4071, #4070), one optional and performance only (#4072). 4.5 changed no schema.
| Script | Status | Adds |
|---|---|---|
migrations/4.6/add_scheduler_version_<db>.sql | Mandatory for 4.6 and later | QUARTZ_VERSION on QRTZ_SCHEDULER_STATE |
migrations/4.6/add_fired_trigger_interrupt_<db>.sql | Mandatory for 4.6 and later | INTERRUPT_REQUESTED_TIME on QRTZ_FIRED_TRIGGERS |
migrations/4.6/add_trigger_misfire_threshold_<db>.sql | Mandatory for 4.6 and later | MISFIRE_THRESHOLD on QRTZ_TRIGGERS |
migrations/4.6/add_execution_history_lookup_indexes_<db>.sql | Optional: performance only | IDX_QRTZ_EH_FIRE_INST and IDX_QRTZ_EH_TRIG_TIME on QRTZ_EXECUTION_HISTORY |
The version column
QUARTZ_VERSION is the Quartz version the node runs, as major.minor.patch (4.6.0), written with LAST_CHECKIN_TIME at every check-in.
| Value | Meaning |
|---|---|
NULL | A node older than 4.6, which never writes the column |
4.6.0 or later | A node whose misfire and edit writes are held to the row they read |
- Nullable, no default,
VARCHAR(20)on every dialect (nvarchar(20)on SQL Server,VARCHAR2(20)on Oracle). A prerelease tag is not stored. - A 4.6 node fires a round of triggers whose jobs allow concurrent execution without the cluster's
TRIGGER_ACCESSlock, but only while every live row of this table carries 4.6 or later. See Lock-free firing rounds. - No index. The column is read with the row the check-in already reads.
- A 4.6 node refuses to start without it; the startup check names the column and the script.
The interrupt column
INTERRUPT_REQUESTED_TIME is when another node asked for the firing to be interrupted, in ticks like FIRED_TIME. NULL is a firing nobody asked to interrupt.
- Nullable, no default;
BIGINT(NUMBER(19)on Oracle). - Written by
IScheduler.Interrupt,InterruptFireInstanceand aCancelPreviousfire on a clustered store, for an executing firing another node runs. Read by the node running it after each check-in, which cancels the job's token. See Interrupting a firing on another node. - The row is deleted when the firing completes, request and all.
- No index. The read filters
SCHED_NAMEandINSTANCE_NAME, whichIDX_QRTZ_FT_INST_JOB_REQ_RCVRYleads with, and one node's rows are the firings it holds. - A 4.6 node refuses to start without it; the startup check names the column and the script.
The misfire threshold column
MISFIRE_THRESHOLD is the trigger's own misfire threshold in milliseconds.
| Value | Meaning |
|---|---|
NULL | The store's MisfireThreshold; every row a node older than 4.6 writes |
| a positive number | How late the trigger may fire before it misfires |
- Nullable, no default, a 64-bit integer (
bigint,BIGINT,NUMBER(19)). - No index. The misfire scan and acquisition still range over
IDX_QRTZ_T_NFT_STonNEXT_FIRE_TIME, and read the column as a residual beside it. - A 4.5 node ignores it, so a cluster honours a trigger's threshold only once every node is 4.6. A 4.5 node that updates the row leaves the column as it was.
- A 4.6 node refuses to start without it; the startup check names the column and the script.
Rolling 4.5 → 4.6
Run all three mandatory scripts while 4.5 nodes are still up. All three columns are nullable with no default, and a 4.5 node never names them: its state row reads NULL.
| While | A 4.6 node |
|---|---|
Any live row is NULL, below 4.6 or unreadable | Takes TRIGGER_ACCESS for every firing round, and logs event 3517 once |
| Every live row is 4.6 or later | Fires rounds of concurrent-allowed work without the lock, and logs event 3518 once |
A row past its check-in failure threshold is a node about to be recovered and does not count. Nothing needs configuring for the rollout: roll every node, and each 4.6 node switches on its next check-in after the last 4.5 row ages out. A node downgraded under the same instance id does not keep a 4.6 stamp: its first check-in recovers and deletes its own earlier row before it inserts a fresh one.
A 4.5 node never reads INTERRUPT_REQUESTED_TIME. A request for its firing is recorded and never answered, and a 4.6 node says so once per such node (event 3520). A CancelPrevious firing still waits for one, as before. Interrupting across the cluster needs every node on 4.6.
The history lookup indexes
| Index | Columns | Serves |
|---|---|---|
IDX_QRTZ_EH_FIRE_INST | SCHED_NAME, FIRE_INSTANCE_ID | ExecutionHistoryQuery.FireInstanceId: one firing's outcome |
IDX_QRTZ_EH_TRIG_TIME | SCHED_NAME, TRIGGER_GROUP, TRIGGER_NAME, FIRED_TIME | ExecutionHistoryQuery.Trigger: one trigger's history, newest first |
- No store probes for them. Without them each lookup scans the scheduler's rows: on PostgreSQL over 1,000,000 rows, 60 ms for a firing and 261 ms for a rarely firing trigger, against under 0.2 ms with them.
- On a database without
QRTZ_EXECUTION_HISTORYthe script changes nothing. SQLite cannot check for the table first, so run it there only on a database that has one. - A history created before 4.4 needs
4.4/add_execution_outcome_<db>.sqlfirst:FIRE_INSTANCE_IDis one of its columns. - Safe under a mixed cluster: an index changes plans, not results.
A fresh install from database/tables/, and ProvisionSchema(), create both columns and both indexes.
Version 4.7
One mandatory script (#4140).
| Script | Status | Adds |
|---|---|---|
migrations/4.7/add_trigger_fixed_delay_<db>.sql | Mandatory for 4.7 and later | FIXED_DELAY on QRTZ_TRIGGERS |
The fixed-delay column
FIXED_DELAY says a simple trigger is measured from completion.
| Value | Meaning |
|---|---|
NULL | A fixed rate; every row a node older than 4.7 writes |
| true | The next run is the previous run's completion plus the interval |
- Nullable, no default, a boolean (
bit,BOOL,BOOLEAN,VARCHAR2(1)on Oracle,SMALLINTon Firebird). - On
QRTZ_TRIGGERSrather thanQRTZ_SIMPLE_TRIGGERS: a firing round's candidate read, which reads this table alone, decides from it whether the round takesTRIGGER_ACCESS. - No index. Acquisition reads it beside
NEXT_FIRE_TIME, and nothing searches on it. - A 4.7 node refuses to start without it; the startup check names the column and the script.
Rolling 4.6 → 4.7
Run the script while 4.6 nodes are still up: the column is nullable with no default, and a 4.6 node never names it. A 4.6 node runs a trigger measured from completion at a fixed rate, beside a running firing of it, and leaves the column as it was when it updates the row. Roll every node, then make a trigger measure from completion.
See also
database/README.md: the same table, in the repository- Database Schema: what each table holds
- Migration Guide: the rest of the 3.x → 4.x upgrade
