- Delete inactive data events to manage data size and performance: For example, you may want to delete order records from an online store after 90 days.
- Delete data no longer needed for compliance: For example, a banking application may need to keep some subset of data for a period of time due to financial regulations. Row-Level TTL can be used to remove data older than that period on a rolling, continuous basis.
- Outbox pattern: When events are written to an outbox table and published to an external system like Kafka using CockroachDB’s feature (also known as “changefeeds”), those events must be deleted to prevent unbounded growth in the size of the outbox table.
How it works
At a high level, Row-Level TTL works by:- Issuing a at a , yielding a set of rows that are eligible for deletion (also known as “expired”).
- Issuing batched statements for the expired rows.
- As part of the above process, deciding how many rows to and at once in each of the above queries.
- Running the SQL queries described above in parallel as .
- To minimize the performance impact on foreground application queries, the background deletion queries are rate limited; they are also submitted at a lower priority level using the . When foreground traffic increases, CockroachDB will reduce the resources allocated to TTL deletes to handle the foreground traffic. When foreground traffic decreases, CockroachDB will increase the resources allocated to TTL deletes.
-
Latency of row-level TTL queries is further reduced by using the elastic CPU limiter, which dynamically controls the total CPU percentage used by row-level TTL reads. The elastic CPU limiter can be disabled for row-level TTL queries by setting the
kvadmission.low_pri_read_elastic_control.enabledandsqladmission.low_pri_read_response_elastic_control.enabledtofalse.
When are rows deleted?
Once rows are expired (that is, are older than the specified TTL interval), they are eligible to be deleted. However, eligible rows may not be deleted right away. Instead, they are scheduled for deletion using a background job that is run at the interval defined by thettl_job_cron storage parameter.
Syntax overview
TTLs are defined using either thettl_expiration_expression or ttl_expire_after storage parameters.
- Using
ttl_expiration_expressionis useful for customizing expiration logic by providing an expression. For example, you could get the same behavior asttl_expire_afterby creating a column with a default value and having thettl_expiration_expressionreference that column. - Using
ttl_expire_afteris a convenient way of setting rows to expire a fixed amount of time after they are created or updated.
ttl_expiration_expression instead of ttl_expire_after for the following reasons:
- If you add
ttl_expire_afterto an existing table, it will cause a full table rewrite, which can affect performance. Specifically, it will result in a that (1) creates a newcrdb_internal_expirationfor all rows, and (2) backfills the value of that new column tonow()+ttl_expire_after. - You cannot use
ttl_expire_afterwith an existing column. - If you use
ttl_expiration_expression, you can use an existing column called e.g.updated_at.
Using ttl_expiration_expression
Use ttl_expiration_expression for customizing the expiration logic by providing a SQL expression. For example, you could get the same behavior as ttl_expire_after by creating a column with a default value and having the ttl_expiration_expression reference that column.
To add custom expiration logic using ttl_expiration_expression, issue the following SQL statement that uses the ttl_expiration_expression parameter, which defines a after which the row is considered expired:
- Creates a repeating scheduled job for the table and sets it to run once per day.
- Implicitly adds the
ttlandttl_cronstorage parameters.
ttl_expiration_expression instead of ttl_expire_after for the following reasons:
- If you add
ttl_expire_afterto an existing table, it will cause a full table rewrite, which can affect performance. Specifically, it will result in a that (1) creates a newcrdb_internal_expirationfor all rows, and (2) backfills the value of that new column tonow()+ttl_expire_after. - You cannot use
ttl_expire_afterwith an existing column. - If you use
ttl_expiration_expression, you can use an existing column called e.g.updated_at.
Using ttl_expire_after
To set rows to expire a fixed amount of time after they are created or updated, issue the following SQL statement using the ttl_expire_after storage parameter:
- Creates a repeating scheduled job for the table and sets it to run once per day.
- Adds a
NOT VISIBLEcolumn calledcrdb_internal_expirationof type to represent the TTL. - Implicitly adds the
ttlandttl_cronstorage parameters.
ttl_expiration_expression instead of ttl_expire_after for the following reasons:
- If you add
ttl_expire_afterto an existing table, it will cause a full table rewrite, which can affect performance. Specifically, it will result in a that (1) creates a newcrdb_internal_expirationfor all rows, and (2) backfills the value of that new column tonow()+ttl_expire_after. - You cannot use
ttl_expire_afterwith an existing column. - If you use
ttl_expiration_expression, you can use an existing column called e.g.updated_at.
TTL storage parameters
The settings that control the behavior of Row-Level TTL are provided using . These parameters can be set during table creation usingCREATE TABLE, added to an existing table using the ALTER TABLE statement, or reset to default values.
| Option | Description | Associated cluster setting | |
|---|---|---|---|
ttl_expiration_expression | Recommended. SQL expression that defines the TTL expiration. Must evaluate to a . This and/or ttl_expire_after are required to enable TTL. This parameter is useful when you want to set the TTL for individual rows in the table. For an example, see Create a table with a ttl_expiration_expression. | N/A | |
ttl_expire_after | The when a TTL will expire. This and/or ttl_expiration_expression are required to enable TTL. Minimum value: '1 microsecond'. | N/A | |
ttl | Signifies if a TTL is active. Automatically set. | N/A | |
ttl_select_batch_size | How many rows to at one time during the row expiration check. Defaults to the value of the associated cluster setting if unset. | sql.ttl.default_select_batch_size Default: 500. Minimum: 1. | |
ttl_delete_batch_size | How many rows to at a time. Defaults to the value of the associated cluster setting if unset. | sql.ttl.default_delete_batch_size Default: 100. Minimum: 1. | |
ttl_select_rate_limit | Maximum number of rows to be selected per second (rate limit). Defaults to the value of the associated cluster setting if unset. Note: The rate limit is applied per node per table. The TTL job prefers to assign work to the leaseholder of each range, but this is not guaranteed. In some cases, non-leaseholder nodes may be assigned work. As a result, the effective cluster-wide rate depends on how many nodes end up processing TTL work for the table. | sql.ttl.default_select_rate_limitDefault: 0. Minimum: 0 (unlimited). | |
ttl_delete_rate_limit | Maximum number of rows to be deleted per second (rate limit). Defaults to the value of the associated cluster setting if unset. Note: The rate limit is applied per node per table, with the same leaseholder preference and fallback behavior as ttl_select_rate_limit. The actual cluster-wide rate will vary depending on which nodes execute TTL work. | sql.ttl.default_delete_rate_limitDefault: 100. Minimum: 0 (unlimited). | |
ttl_row_stats_poll_interval | If set, counts rows and expired rows on the table to report as Prometheus metrics while the TTL job is running. Unset by default, meaning no stats are fetched and reported. | N/A | |
ttl_pause | If set, stops the TTL job from executing. | N/A | |
ttl_job_cron | Frequency at which the TTL job runs, specified using CRON syntax. Default: '@daily' (was '@hourly'). | N/A | |
ttl_disable_changefeed_replication | Disables replication for the deletes performed by the TTL job. | sql.ttl.changefeed_replication.disabled |
TTL metrics
The table below lists the metrics you can use to monitor the effectiveness of your TTL settings. These metrics are visible on the , as well as at the which can be scraped by .| Name | Description | Measurement | Type |
|---|---|---|---|
jobs.row_level_ttl.range_total_duration | Duration for processing a range during row level TTL. | nanoseconds | Histogram |
jobs.row_level_ttl.select_duration | Duration for select requests during row level TTL. | nanoseconds | Histogram |
jobs.row_level_ttl.delete_duration | Duration for delete requests during row level TTL. | nanoseconds | Histogram |
jobs.row_level_ttl.rows_selected | Number of rows selected for deletion by the row level TTL job. | num_rows | Counter |
jobs.row_level_ttl.rows_deleted | Number of rows deleted by the row level TTL job. | num_rows | Counter |
jobs.row_level_ttl.num_active_ranges | Number of active workers attempting to delete for row level TTL. | num_active_workers | Count |
jobs.row_level_ttl.total_rows | Approximate number of rows on the TTL table. | total_rows | Count |
jobs.row_level_ttl.total_expired_rows | Approximate number of expired rows on the TTL table. | total_expired_rows | Count |
- Set the
server.child_metrics.enabledtotrue. - Set the
ttl_label_metricsstorage parameter totrue.
For more information about the issues (including negative performance impacts) that can arise when you add cardinality, see the considerations listed in .
Examples
Create a table with a ttl_expiration_expression
Use the SQL syntax shown below, which uses the ttl_expiration_expression parameter to refer to an expire_at column that determines each row’s expiration:
ttl_expiration_expression” parameter takes a SQL expression (often a column name) that defines the TTL expiration. It is used when you want to set the TTL for individual rows in a table.
The ttl_expiration_expression parameter has the following requirements:
- It must evaluate to a .
- It must not reference any columns outside the table to which it is applied.
- Any column it references cannot be or have its .
- Finally, if the , the value of
ttl_expiration_expressionis automatically updated.
Use a ttl_expiration_expression on a DATE or TIMESTAMPTZ column
Use the SQL syntax shown below to create a new table with rows that expire 30 days after an event ends using a ttl_expiration_expression.
A ttl_expiration_expression that uses an existing DATE column:
ttl_expiration_expression that uses an existing TIMESTAMPTZ column:
ttl_expiration_expression on a DATE or TIMESTAMPTTZ column, use AT TIME ZONE to explicitly set the time zone for the expression. By setting the time zone to UTC in the expression, you set an exact time when the delete should be performed, regardless of the local time zone of the node.
Create a table with ttl_expire_after
Use the SQL syntax shown below to create a new table with rows that expire after a 3 month , execute a statement like the following:
Add or update the row-level TTL for an existing table
To add or change the row-level TTL expiration for an existing table, use as shown in the following example. This example assumes you have an existing or column you can use for thettl_expiration_expression.
View scheduled TTL jobs
You can use to view all TTL-related scheduled jobs by executing the following query:View running TTL jobs
You can use to see any running TTL jobs by executing the following query:View TTL storage parameters on a table
To view TTL storage parameters on a table, you can use :Control how often the TTL job runs
Setting a TTL on a table controls when the rows therein are considered expired, but it only says that such rows may be deleted at any time after the expiration. To control how often the TTL deletion job runs, use thettl_job_cron storage parameter, which supports CRON syntax. Cockroach Labs recommends setting ttl_job_cron to be equal to or longer than the setting, which is the garbage collection interval for the cluster. The default value of gc.ttlseconds is 14400, or 4 hours. The CRON pattern for every four hours is '0 */4 * * *'.
To control the job interval at time, add the storage parameter as shown below:
To set the
ttl_job_cron storage parameter when creating a table with Row-Level TTL, you must also set either the ttl_expire_after parameter or the ttl_expiration_expression parameter.Pause the TTL job from running
To pause the TTL job from running on a table, use thettl_pause storage parameter:
Filter out expired rows from a selection query
To fetch only those rows from a table with table-wide TTL that have not yet expired their TTL, use the hiddencrdb_internal_expiration column:
ttl_expiration_expression that have not yet expired their TTL, use the expired_at column you created earlier:
Reset a storage parameter to its default value
To reset a TTL storage parameter to its default value, use the statement:Remove Row-Level TTL from a table
To drop the TTL on an existing table, reset thettl storage parameter.
ttl_expire_after and ttl_expiration_expression are set, and you want to remove one or the other, you can use either of:
Disable TTL jobs for the whole cluster
To disable TTL jobs for the whole cluster, set thesql.ttl.job.enabled to false:
View TTL-related cluster settings
To view the that control how Row-Level TTL works, issue the following query:Common errors
If you attempt to update a TTL storage parameter on a table that does not have TTL enabled, you will get an error as shown below:ttl_expire_after storage parameter you set earlier, you will get the following error:
Changefeeds
Row-level TTL interacts with in the following ways:- When expired rows are deleted, a is emitted.
Filter changefeeds for tables using row-level TTL
Use thettl_disable_changefeed_replication table storage parameter to prevent changefeeds from sending DELETE messages issued by row-level TTL jobs for a table. Include the storage parameter when you create or alter the table. For example:
sql.ttl.changefeed_replication.disabled to true. This will prevent changefeeds from emitting deletes issued by all TTL jobs on a cluster.
If you want to have a changefeed ignore the storage parameter or cluster setting that disables changefeed replication, you can set the changefeed option ignore_disable_changefeed_replication to true:
Backup and restore
Row-level TTL interacts with in the following ways:- When you run a , all row-level TTL information associated with the tables being backed up (including TTL expiration times) is also backed up.
- When you from a backup, all row-level TTL information associated with the tables being restored (including TTL expiration times) is also restored. Any expired rows in the restored tables are eligible to be deleted by the TTL job.
Required Privileges
To add or update Row-Level TTL settings on a table, you must have one of the following:- Membership to the role for the database where the table is located.
- The on the database where the table is located.
Migrating TTL usage from earlier versions of CockroachDB
If you are migrating your TTL usage from an earlier version of CockroachDB, thettl_expire_after and ttl_expiration_expression storage parameters can co-exist where the ttl_expire_after creates the crdb_internal_expiration column and ttl_expiration_expression overrides the default value of crdb_internal_expiration.
Known limitations
- Any queries you run against tables with Row-Level TTL enabled (or against tables that have that reference TTL-enabled tables) do not filter out expired rows from the result set (this includes and ). This feature may be added in a future release. For now, follow the instructions in .
- Enabling Row-Level TTL on a table with multiple can have negative performance impacts on a cluster, including increased and . This is particularly true for large tables with terabytes of data and billions of rows that are split up into multiple ranges across separate nodes.
- Increased latency may occur because secondary indexes aren’t necessarily stored on the same underlying as a table’s . Further, the secondary indexes’ ranges may have located on different nodes than the primary index.
- Increased contention may occur because must be written as part of performing the deletions.
- Finally, secondary indexes can also have a negative impact on the overall performance of . According to internal testing, the is worse on tables with secondary indexes. If you encounter this situation, decreasing the may help by decreasing the number of ranges that need to be accessed by the job.

