diff --git a/documentation/architecture/storage-engine.md b/documentation/architecture/storage-engine.md
index 5f2335618..ada358aef 100644
--- a/documentation/architecture/storage-engine.md
+++ b/documentation/architecture/storage-engine.md
@@ -55,9 +55,10 @@ Older partitions (any partition other than the most recent one) can be converted
Partitions in Parquet format remain fully available for queries. Users don't need to know whether a partition is in QuestDB
binary format or Parquet format. All the data types available in QuestDB can be converted to Parquet.
-When using QuestDB Enterprise, tables can be configured to convert to Parquet automatically and to send the Parquet
-files to object storage (Amazon S3, Microsoft Blob Storage, Google Cloud Storage, NFS...). This can help reduce the
-cost of storing historical data while keeping it fully available for queries.
+When using QuestDB Enterprise, tables can be configured to convert to Parquet automatically using
+[storage policies](/docs/concepts/storage-policy/). This can help reduce local disk usage
+while keeping historical data fully available for queries. Support for automatic upload
+of Parquet files to object storage will be added in a future release.
diff --git a/documentation/concepts/materialized-views.md b/documentation/concepts/materialized-views.md
index 4ae5c248d..92dd94bb7 100644
--- a/documentation/concepts/materialized-views.md
+++ b/documentation/concepts/materialized-views.md
@@ -336,6 +336,16 @@ CREATE MATERIALIZED VIEW trades_hourly AS (
The view's TTL is independent of the base table's TTL.
+:::note
+
+In QuestDB Enterprise, TTL is superseded by
+[storage policies](/docs/concepts/storage-policy/). Use
+[`STORAGE POLICY(...)`](/docs/query/sql/alter-mat-view-set-storage-policy/) on
+a materialized view instead of `TTL` for graduated lifecycle management
+(convert to Parquet, then drop).
+
+:::
+
### Initial refresh
When created, materialized views start an **asynchronous full refresh**:
@@ -592,6 +602,9 @@ the replica's view was not fully up-to-date.
Sets the time limit for incremental refresh on a materialized view
- [`ALTER MATERIALIZED VIEW SET TTL`](/docs/query/sql/alter-mat-view-set-ttl/):
Sets the time-to-live (TTL) period on a materialized view
+ - [`ALTER MATERIALIZED VIEW SET STORAGE POLICY`](/docs/query/sql/alter-mat-view-set-storage-policy/):
+ Attaches a [storage policy](/docs/concepts/storage-policy/) to a
+ materialized view (QuestDB Enterprise)
- **Configuration**
- [Materialized views configs](/docs/configuration/overview/#materialized-views):
diff --git a/documentation/concepts/partitions.md b/documentation/concepts/partitions.md
index decbef1f6..6aed44d68 100644
--- a/documentation/concepts/partitions.md
+++ b/documentation/concepts/partitions.md
@@ -189,3 +189,5 @@ partition as a single unit.
- [DETACH PARTITION](/docs/query/sql/alter-table-detach-partition/) — Move to cold storage
- [ATTACH PARTITION](/docs/query/sql/alter-table-attach-partition/) — Restore detached data
- [TTL](/docs/concepts/ttl/) — Automatic partition cleanup by age
+- [Storage Policy](/docs/concepts/storage-policy/) — Graduated partition
+ lifecycle (convert to Parquet, then drop) in QuestDB Enterprise
diff --git a/documentation/concepts/storage-policy.md b/documentation/concepts/storage-policy.md
new file mode 100644
index 000000000..aaa27a490
--- /dev/null
+++ b/documentation/concepts/storage-policy.md
@@ -0,0 +1,363 @@
+---
+title: Storage Policy
+sidebar_label: Storage Policy
+description: Automate partition lifecycle in QuestDB Enterprise - convert to Parquet locally and drop old data on a schedule.
+---
+
+:::note
+
+Storage policies are available in **QuestDB Enterprise** only.
+
+:::
+
+A storage policy automates the lifecycle of table partitions. It defines when
+partitions are converted to Parquet, when native data is removed, and when local
+copies are dropped. This replaces the need for manual partition management or
+external scheduling.
+
+:::info
+
+Storage policies currently operate **locally only**. Parquet files are not
+automatically uploaded to object storage, and the `DROP REMOTE` clause is
+reserved syntax — it is rejected at SQL parse time with
+`'DROP REMOTE' is not supported yet`. Accordingly, the `drop_remote`
+column in the [`storage_policies`](/docs/query/functions/meta/#storage_policies)
+view is always blank in the current release; it is kept for forward
+compatibility. Object storage integration will be added in a future release.
+
+:::
+
+## Requirements
+
+Storage policies require:
+
+- A [designated timestamp](/docs/concepts/designated-timestamp/) column
+- [Partitioning](/docs/concepts/partitions/) enabled
+- QuestDB Enterprise
+
+## How it works
+
+A storage policy consists of up to four TTL settings. Each setting controls a
+stage in the partition lifecycle:
+
+| Setting | Description |
+|---------|-------------|
+| `TO PARQUET` | Convert the partition from native binary format to Parquet |
+| `DROP NATIVE` | Remove native binary files, keeping only the local Parquet copy |
+| `DROP LOCAL` | Remove all local data (both native and Parquet) |
+| `DROP REMOTE` | _Reserved._ Will remove the Parquet file from object storage when remote upload is supported |
+
+All settings are optional. Use only the ones relevant to your use case. All TTL
+values must be **positive**; `0` is rejected.
+
+### Partition lifecycle
+
+As time passes, each partition progresses through the stages defined by the
+policy:
+
+```text
+ TO PARQUET DROP NATIVE DROP LOCAL
+ [Native] ──────────┬──────────────────┬──────────────────┬───────
+ │ │ │
+ ▼ ▼ ▼
+ Native + Parquet Parquet only Data removed
+ (local) (local)
+```
+
+### TTL evaluation
+
+Storage policy TTLs follow the same evaluation rules as
+[TTL](/docs/concepts/ttl/). A partition becomes eligible for a lifecycle action
+when its **entire time range** falls outside the TTL window:
+
+```text
+eligible when: partition_end_time < reference_time - TTL
+```
+
+**This rule is applied independently for each stage's TTL.** A partition can
+be eligible for `TO PARQUET` long before it is eligible for `DROP NATIVE`,
+`DROP LOCAL`, or (one day) `DROP REMOTE`. Each stage uses its own `TTL` in the
+formula above; the stages share only the reference time and the ordering
+constraint `TO PARQUET <= DROP NATIVE <= DROP LOCAL <= DROP REMOTE`.
+
+The reference time is `min(wall_clock_time, latest_timestamp)` by default —
+the same formula used by TTL. The
+[`cairo.ttl.use.wall.clock`](/docs/concepts/ttl/#restore-legacy-behavior)
+setting applies to storage policies as well: setting it to `false` removes
+the wall-clock cap for both TTL and storage policy evaluation. See
+[TTL § Reference time](/docs/concepts/ttl/#reference-time) for the rationale
+and the data-loss hazard of disabling the cap.
+
+QuestDB checks storage policies periodically (every 15 minutes by default) and
+processes eligible partitions automatically.
+
+## Storage policy vs TTL
+
+Storage policies replace [TTL](/docs/concepts/ttl/) in QuestDB Enterprise. If
+you are already familiar with TTL, this comparison is the fastest way in:
+
+| | TTL | Storage Policy |
+|---|-----|----------------|
+| **Availability** | Open source | Enterprise only |
+| **Action** | Drops partitions entirely | Graduated lifecycle (convert, then drop) |
+| **Parquet conversion** | No | Yes (automatic local conversion) |
+| **Granularity** | Single retention window | Up to four independent TTL stages |
+
+In QuestDB Enterprise, `CREATE TABLE ... TTL` and `ALTER TABLE SET TTL` are
+deprecated. Use storage policies instead:
+
+```questdb-sql
+-- Instead of:
+-- ALTER TABLE trades SET TTL 30 DAYS;
+
+-- Use:
+ALTER TABLE trades SET STORAGE POLICY(DROP LOCAL 30d);
+```
+
+:::note
+
+If a table already has a TTL set, you must clear it with
+`ALTER TABLE SET TTL 0` before setting a storage policy. `SET TTL 0` is the
+only `SET TTL` value Enterprise accepts; any non-zero value is rejected with
+`TTL settings are deprecated, please, create a storage policy instead`.
+
+:::
+
+## Setting a storage policy
+
+### At table creation
+
+```questdb-sql
+CREATE TABLE trades (
+ ts TIMESTAMP,
+ symbol SYMBOL,
+ price DOUBLE
+) TIMESTAMP(ts) PARTITION BY DAY
+ STORAGE POLICY(TO PARQUET 3d, DROP NATIVE 10d, DROP LOCAL 1M)
+ WAL;
+```
+
+### On existing tables
+
+```questdb-sql
+ALTER TABLE trades SET STORAGE POLICY(
+ TO PARQUET 3 DAYS,
+ DROP NATIVE 10 DAYS,
+ DROP LOCAL 1 MONTH
+);
+```
+
+Only the specified settings are changed. Omitted settings remain unchanged.
+
+### On materialized views
+
+```questdb-sql
+CREATE MATERIALIZED VIEW hourly_trades AS (
+ SELECT ts, symbol, sum(price) total
+ FROM trades
+ SAMPLE BY 1h
+) PARTITION BY DAY
+ STORAGE POLICY(TO PARQUET 7d, DROP NATIVE 14d);
+```
+
+```questdb-sql
+ALTER MATERIALIZED VIEW hourly_trades SET STORAGE POLICY(TO PARQUET 7d);
+```
+
+For full syntax details, see
+[ALTER TABLE SET STORAGE POLICY](/docs/query/sql/alter-table-set-storage-policy/).
+
+## TTL duration format
+
+Storage policy TTLs accept the same duration formats as
+[TTL](/docs/concepts/ttl/):
+
+| Unit | Long form | Short form |
+|------|-----------|------------|
+| Hours | `1 HOUR` / `2 HOURS` | `1h` |
+| Days | `1 DAY` / `3 DAYS` | `1d` / `3d` |
+| Weeks | `1 WEEK` / `2 WEEKS` | `1W` / `2W` |
+| Months | `1 MONTH` / `6 MONTHS` | `1M` / `6M` |
+| Years | `1 YEAR` / `2 YEARS` | `1Y` / `2Y` |
+
+### Ordering constraint
+
+TTL values must be in ascending order:
+
+```text
+TO PARQUET <= DROP NATIVE <= DROP LOCAL <= DROP REMOTE
+```
+
+For example, you cannot drop native files before the Parquet conversion
+completes. All TTL values must be positive — `0` is rejected.
+
+## Disabling and enabling
+
+Temporarily suspend a storage policy without removing it:
+
+```questdb-sql
+ALTER TABLE trades DISABLE STORAGE POLICY;
+```
+
+Re-enable it later:
+
+```questdb-sql
+ALTER TABLE trades ENABLE STORAGE POLICY;
+```
+
+Both `ENABLE` and `DISABLE` require a policy to exist on the table; the
+statement returns an error otherwise.
+
+## Removing a storage policy
+
+To permanently remove a storage policy from a table:
+
+```questdb-sql
+ALTER TABLE trades DROP STORAGE POLICY;
+```
+
+## Checking storage policies
+
+Query the `storage_policies` system view to see all active policies:
+
+```questdb-sql
+SELECT * FROM storage_policies;
+```
+
+| table_dir_name | to_parquet | drop_native | drop_local | drop_remote | status | last_updated |
+|----------------|-----------|-------------|------------|-------------|--------|--------------|
+| trades~12 | 72h | 240h | 1m | | A | 2025-01-15T10:30:00.000000Z |
+
+- TTL values are rendered in just two units: `h` for hours and `m` for
+ **months**. Hour-, day-, and week-based durations are normalized to hours
+ when stored, so a `3 DAYS` TTL appears as `72h` and `1 WEEK` appears as
+ `168h`. Month-based durations keep the lowercase `m` suffix — **`1m` in
+ this view means one month, not one minute**; QuestDB's duration shorthand
+ has no unit for minutes
+- Status `A` means active; `D` means disabled (see
+ [Disabling and enabling](#disabling-and-enabling))
+- Unset stages appear blank. `drop_remote` is **always blank in the current
+ release** because `DROP REMOTE` is rejected at SQL parse time with
+ `'DROP REMOTE' is not supported yet`; the column is kept for forward
+ compatibility
+
+For the full column reference and types, see
+[`storage_policies`](/docs/query/functions/meta/#storage_policies).
+
+## Replication
+
+Storage policy definitions are persisted in WAL-backed system tables, so the
+policy itself is replicated to every instance in the cluster. Enforcement runs
+**independently on each instance** — Parquet files are produced locally and
+are not replicated.
+
+This means the primary and its replicas can temporarily disagree on which
+partitions have been converted to Parquet or dropped, depending on when each
+node's storage policy [check interval](#configuration) last fired. The state
+converges as each instance processes its own queue. See
+[Replication overview](/docs/high-availability/overview/#storage-policies-in-a-replicated-cluster)
+for details.
+
+## Configuration
+
+Storage policy behavior can be tuned in `server.conf`. Time-based properties
+accept values with unit suffixes (e.g., `15m`, `30s`, `1h`) or raw microsecond
+values:
+
+| Property | Default | Description |
+|----------|---------|-------------|
+| `storage.policy.check.interval` | `15m` (15 min) | How often QuestDB scans for partitions to process |
+| `storage.policy.retry.interval` | `1m` (1 min) | Retry interval for failed tasks |
+| `storage.policy.max.reschedule.count` | `20` | Maximum retries before abandoning a task |
+| `storage.policy.writer.wait.timeout` | `30s` (30 sec) | Timeout for acquiring the table writer |
+| `storage.policy.worker.count` | `2` | Number of storage policy worker threads (0 disables the feature) |
+| `storage.policy.worker.affinity` | `-1` (no affinity) | CPU affinity for each worker thread (comma-separated list) |
+| `storage.policy.worker.sleep.timeout` | `100ms` | Sleep duration when worker has no tasks |
+
+## Permissions
+
+Storage policy operations require specific permissions in QuestDB Enterprise:
+
+| Operation | Required permission |
+|-----------|-------------------|
+| `SET STORAGE POLICY` | `SET STORAGE POLICY` |
+| `DROP STORAGE POLICY` | `REMOVE STORAGE POLICY` |
+| `ENABLE STORAGE POLICY` | `ENABLE STORAGE POLICY` |
+| `DISABLE STORAGE POLICY` | `DISABLE STORAGE POLICY` |
+
+Grant permissions using standard RBAC syntax:
+
+```questdb-sql
+GRANT SET STORAGE POLICY ON trades TO analyst;
+GRANT REMOVE STORAGE POLICY ON trades TO admin;
+```
+
+## End-to-end example
+
+A complete lifecycle on a single table, from creation through verification,
+modification, inspection of the generated DDL, temporary suspension, and
+permanent removal:
+
+```questdb-sql title="1. Create the table with a storage policy"
+CREATE TABLE trades (
+ ts TIMESTAMP,
+ symbol SYMBOL,
+ price DOUBLE
+) TIMESTAMP(ts) PARTITION BY DAY
+ STORAGE POLICY(TO PARQUET 3d, DROP NATIVE 10d, DROP LOCAL 1M)
+ WAL;
+```
+
+```questdb-sql title="2. Verify via the system view"
+SELECT table_dir_name, to_parquet, drop_native, drop_local, status
+FROM storage_policies
+WHERE table_dir_name LIKE 'trades%';
+```
+
+| table_dir_name | to_parquet | drop_native | drop_local | status |
+|----------------|------------|-------------|------------|--------|
+| trades~12 | 72h | 240h | 1m | A |
+
+```questdb-sql title="3. Modify one stage (others remain unchanged)"
+ALTER TABLE trades SET STORAGE POLICY(TO PARQUET 1d);
+```
+
+```questdb-sql title="4. Inspect the current DDL"
+SHOW CREATE TABLE trades;
+```
+
+```text
+CREATE TABLE 'trades' (
+ ts TIMESTAMP,
+ symbol SYMBOL CAPACITY 256 CACHE,
+ price DOUBLE
+) timestamp(ts) PARTITION BY DAY
+STORAGE POLICY(TO PARQUET 1 DAY, DROP NATIVE 10 DAYS, DROP LOCAL 1 MONTH) WAL;
+```
+
+```questdb-sql title="5. Temporarily suspend the policy (e.g. during a backfill)"
+ALTER TABLE trades DISABLE STORAGE POLICY;
+-- status in storage_policies changes to 'D'
+```
+
+```questdb-sql title="6. Re-enable and, later, drop it for good"
+ALTER TABLE trades ENABLE STORAGE POLICY;
+ALTER TABLE trades DROP STORAGE POLICY;
+-- row disappears from storage_policies
+```
+
+## Guidelines
+
+| Use case | Suggested policy | Rationale |
+|----------|-----------------|-----------|
+| Real-time metrics | `TO PARQUET 1d, DROP NATIVE 7d, DROP LOCAL 30d` | Keep recent data fast, drop old data automatically |
+| Trading data | `TO PARQUET 7d, DROP NATIVE 30d` | Keep Parquet locally for long-term queries |
+| IoT telemetry | `TO PARQUET 1d, DROP NATIVE 3d, DROP LOCAL 90d` | High volume, convert early to save disk; keep a brief native overlap for in-flight queries before dropping the native files |
+| Aggregated views | `TO PARQUET 30d` | Low volume, keep locally in Parquet |
+
+**Tips:**
+
+- Start with `TO PARQUET` and `DROP NATIVE` to reduce local disk usage while
+ keeping data queryable in Parquet format
+- Use `DROP LOCAL` with care as it permanently removes data from the local disk
+- TTL values should be significantly larger than the partition interval
diff --git a/documentation/concepts/ttl.md b/documentation/concepts/ttl.md
index d4db9c510..d7b9e34ab 100644
--- a/documentation/concepts/ttl.md
+++ b/documentation/concepts/ttl.md
@@ -8,6 +8,20 @@ TTL (Time To Live) automatically drops old partitions based on data age. Set a
retention period, and QuestDB removes partitions that fall entirely outside that
window - no cron jobs or manual cleanup required.
+:::caution
+
+**QuestDB Enterprise: TTL is superseded by
+[Storage Policy](/docs/concepts/storage-policy/).** Enterprise rejects any
+non-zero `SET TTL` with
+`TTL settings are deprecated, please, create a storage policy instead`.
+Storage policies extend TTL with graduated lifecycle management (convert to
+Parquet, then drop) and are the recommended retention primitive for Enterprise
+users. The rest of this page describes TTL behavior on QuestDB Open Source
+(and the `SET TTL 0` case on Enterprise, used to clear an older TTL before
+attaching a storage policy).
+
+:::
+
import Screenshot from "@theme/Screenshot"
+## Storage policy
+
+:::note
+
+Storage policy is [Enterprise](/enterprise/) only.
+
+:::
+
+Storage policies automate partition lifecycle management, including local
+deletion and cold storage offloading.
+
+For details, see the
+[storage policy concept](/docs/concepts/storage-policy/) page.
+
+
+
## Logging & Metrics
The following settings are available in `server.conf`:
diff --git a/documentation/getting-started/enterprise-quick-start.md b/documentation/getting-started/enterprise-quick-start.md
index 03655aa13..1e1f64483 100644
--- a/documentation/getting-started/enterprise-quick-start.md
+++ b/documentation/getting-started/enterprise-quick-start.md
@@ -30,7 +30,8 @@ inform your own unique choices.
[6. Query data, PostgreSQL query](#6-query-data-postgresql-query)\
[7. Setup replication](#7-setup-replication)\
[8. Enable compression](#8-enable-compression)\
-[9. Double-check kernel limits](#9-double-check-kernel-limits)\
+[9. Configure a storage policy](#9-configure-a-storage-policy)\
+[10. Double-check kernel limits](#10-double-check-kernel-limits)\
[Next steps](#next-steps)\
[FAQ](#faq)
@@ -467,7 +468,95 @@ of Kubernetes is supported.
For more on storage and compression, see [Enable compression with ZFS](/docs/deployment/compression-zfs/).
-## 9. Double-check kernel limits
+## 9. Configure a storage policy
+
+[Storage policies](/docs/concepts/storage-policy/) are the Enterprise primitive
+for automated partition retention. A policy converts older partitions to
+Parquet, drops the native binary files, and eventually drops the Parquet files
+too — on a schedule you define. This supersedes plain TTL in Enterprise, where
+`ALTER TABLE SET TTL` with a non-zero value is rejected.
+
+### Migrating from TTL when upgrading from OSS
+
+Tables and materialized views that were created in OSS keep their existing
+`TTL` setting after you upgrade to Enterprise — no data is lost at upgrade
+time. However, Enterprise rejects any **new** `TTL` changes (both
+`CREATE ... TTL` and `ALTER ... SET TTL `) with:
+
+```
+TTL settings are deprecated, please, create a storage policy instead
+```
+
+To move a legacy table or materialized view from `TTL` to a storage policy:
+
+1. **Clear the existing TTL** by setting it to `0`. This is the only `SET TTL`
+ value Enterprise accepts, and it is required before a storage policy can be
+ attached:
+
+ ```questdb-sql title="Clear the legacy TTL"
+ ALTER TABLE trades SET TTL 0;
+ -- or, for a materialized view:
+ ALTER MATERIALIZED VIEW trades_hourly SET TTL 0;
+ ```
+
+2. **Attach a storage policy** that reproduces — and ideally extends — the
+ retention the TTL used to provide. A policy lets you keep data in Parquet
+ after you would previously have dropped it, so `DROP LOCAL` (or
+ `DROP NATIVE` if you don't want Parquet at all) is the stage that replaces
+ the old TTL horizon:
+
+ ```questdb-sql title="Replace a 1-month TTL with an equivalent policy"
+ ALTER TABLE trades SET STORAGE POLICY(
+ TO PARQUET 3 DAYS,
+ DROP NATIVE 10 DAYS,
+ DROP LOCAL 1 MONTH
+ );
+ ```
+
+ If you want the policy to behave exactly like the old TTL (delete the
+ partition outright after the same interval), use a single-stage policy —
+ for example `STORAGE POLICY(DROP NATIVE 1 MONTH)` to match `TTL 1 MONTH`.
+
+Do this for every table and materialized view you want to keep managed
+automatically. Tables without a storage policy retain their data indefinitely
+once their legacy TTL has been cleared.
+
+### Creating new tables with a storage policy
+
+Attach a policy at table creation:
+
+```questdb-sql title="Web Console - Create a table with a storage policy"
+CREATE TABLE trades (
+ ts TIMESTAMP,
+ symbol SYMBOL,
+ price DOUBLE
+) TIMESTAMP(ts) PARTITION BY DAY
+ STORAGE POLICY(TO PARQUET 3d, DROP NATIVE 10d, DROP LOCAL 1M)
+ WAL;
+```
+
+Or attach a policy to an existing table:
+
+```questdb-sql title="Web Console - Set a storage policy on an existing table"
+ALTER TABLE trades SET STORAGE POLICY(
+ TO PARQUET 3 DAYS,
+ DROP NATIVE 10 DAYS,
+ DROP LOCAL 1 MONTH
+);
+```
+
+Check active policies via the `storage_policies` system view:
+
+```questdb-sql
+SELECT * FROM storage_policies;
+```
+
+For the full concept, including the stage model, timing, and replication
+behavior, see
+[Storage Policy](/docs/concepts/storage-policy/) and
+[ALTER TABLE SET STORAGE POLICY](/docs/query/sql/alter-table-set-storage-policy/).
+
+## 10. Double-check kernel limits
QuestDB works together with your server operating system to achieve maximum
performance. Prior to putting your server under heavy loads, consider checking
diff --git a/documentation/getting-started/migrate-to-enterprise.md b/documentation/getting-started/migrate-to-enterprise.md
index 04087663b..7056619af 100644
--- a/documentation/getting-started/migrate-to-enterprise.md
+++ b/documentation/getting-started/migrate-to-enterprise.md
@@ -16,8 +16,9 @@ stays in place and works immediately.
- **Role-based access control (RBAC)** with users, groups, and permissions
- **Single Sign-On (SSO)** via OpenID Connect
- **Database replication** for high availability
-- **Multi-tier storage** with seamless object storage integration
-- **Automated backup and recovery** for data protection
+- **[Storage policies](/docs/concepts/storage-policy/)** for automated partition
+ lifecycle management (convert to Parquet, then drop on a schedule)
+- **Automated [backup and recovery](/docs/operations/backup/)** for data protection
## Upgrade steps
diff --git a/documentation/high-availability/overview.md b/documentation/high-availability/overview.md
index 14bf49bfd..c36f38bde 100644
--- a/documentation/high-availability/overview.md
+++ b/documentation/high-availability/overview.md
@@ -79,6 +79,28 @@ cases.
Tables without timestamps (typically used for reference/lookup data) are not
replicated automatically and should be populated separately on each instance.
+## Storage policies in a replicated cluster
+
+[Storage policy](/docs/concepts/storage-policy/) definitions are stored in
+WAL-backed system tables, so the policy itself — the `TO PARQUET`,
+`DROP NATIVE`, and `DROP LOCAL` TTLs and the active/disabled status — is
+replicated to every instance through the same WAL pipeline as user data.
+
+Enforcement, however, runs **independently on each instance**. Parquet files
+are produced locally and are not replicated; each node's storage policy job
+schedules its own `PARQUET_CONVERSION`, `PARQUET_COMMIT`, and `DROP_LOCAL`
+work against its local data. As a result:
+
+- At any given moment a partition may be in different states across the
+ primary and its replicas (e.g. already converted on the primary but still
+ native on a replica that hasn't yet hit its check interval).
+- These differences are temporary. Once each instance's check job runs and
+ processes the partition, the cluster converges to the same logical state.
+- Tuning the [check interval](/docs/concepts/storage-policy/#configuration)
+ (`storage.policy.check.interval`) or worker count
+ (`storage.policy.worker.count`) per instance lets you trade conversion
+ latency against background load on that node.
+
## Bring Your Own Cloud (BYOC)
QuestDB Enterprise can be self-managed or operated by QuestDB's team under the
diff --git a/documentation/operations/backup.md b/documentation/operations/backup.md
index 08bfe029f..012592d4e 100644
--- a/documentation/operations/backup.md
+++ b/documentation/operations/backup.md
@@ -332,6 +332,31 @@ the object store. Backups are stored under `backup//`.
To find your instance name, see [Backup instance name](#backup-instance-name).
+### Interaction with storage policies
+
+[Storage policies](/docs/concepts/storage-policy/) operate locally — they
+convert partitions to Parquet in place and then drop native (and eventually
+local Parquet) files on a schedule. Backups capture whatever is on local disk
+at the time the backup runs:
+
+- Partitions still in native format are backed up as native files.
+- Partitions that have been converted to Parquet (via the `TO PARQUET` stage,
+ after `DROP NATIVE` has fired) are backed up as Parquet files.
+- Once `DROP LOCAL` fires and removes a partition from local disk, subsequent
+ backups will no longer contain that partition — restoring an earlier backup
+ is the only way to recover it.
+
+Plan retention (`backup.cleanup.keep.latest.n`) with your storage policy's
+`DROP LOCAL` TTL in mind: a partition is only recoverable from a backup that
+was taken **before** `DROP LOCAL` removed it from disk. If you need to keep
+historical partitions available for restore, make sure your oldest retained
+backup predates the earliest `DROP LOCAL` fire.
+
+Storage policies run per-instance, so primaries and replicas may disagree on
+which partitions are native vs. Parquet at any given moment. Typically,
+backing up the primary is sufficient (see the bullet on
+primary/replica backups below).
+
### Limitations
- **Database-wide only**: Backup captures the entire database. You cannot
diff --git a/documentation/operations/data-retention.md b/documentation/operations/data-retention.md
index 18dcfb4c9..7d69a978f 100644
--- a/documentation/operations/data-retention.md
+++ b/documentation/operations/data-retention.md
@@ -10,11 +10,17 @@ over time. If stale data is no longer required, users can delete old data from
QuestDB to either save disk space or adhere to a data retention policy. This is
achieved in QuestDB by removing data partitions from a table.
-QuestDB offers two approaches for data retention:
-
-- **Automatic**: Use [Time To Live (TTL)](/docs/concepts/ttl/) to automatically
- drop partitions when data ages beyond a specified threshold. This is the
- simplest approach for most use cases.
+QuestDB offers three approaches for data retention:
+
+- **TTL** _(automatic, open source)_: Use
+ [Time To Live (TTL)](/docs/concepts/ttl/) to automatically drop partitions when
+ data ages beyond a specified threshold. This is the simplest approach and is
+ available in both open source and Enterprise editions.
+- **Storage policy** _(automatic, Enterprise only)_: Use a
+ [storage policy](/docs/concepts/storage-policy/) to automate the partition
+ lifecycle — convert to Parquet locally and drop old data on a schedule. This is
+ the recommended approach for QuestDB Enterprise users who need graduated data
+ tiering beyond simple deletion.
- **Manual**: Use `DROP PARTITION` commands as described on this page for
explicit control over which partitions to remove and when.
diff --git a/documentation/query/export-parquet.md b/documentation/query/export-parquet.md
index 989ed9bcc..fc5c7c3d2 100644
--- a/documentation/query/export-parquet.md
+++ b/documentation/query/export-parquet.md
@@ -18,6 +18,17 @@ All methods compress with `lz4_raw` by default. See [Data Compression](#data-com
To read and query Parquet files, see the [`read_parquet` function](/docs/query/functions/parquet/).
+:::tip
+
+In QuestDB Enterprise, in-place Parquet conversion can be **automated** via
+[storage policies](/docs/concepts/storage-policy/). A storage policy runs the
+conversion on a schedule (e.g., convert to Parquet after 3 days), and can also
+drop the native files and, later, the Parquet files. The manual `ALTER TABLE
+CONVERT PARTITION TO PARQUET` approach described below remains available on
+both OSS and Enterprise.
+
+:::
+
## Export via REST
The `/exp` REST API endpoint executes a query and streams the result as a Parquet file directly to the client. This is a synchronous operation — the HTTP response completes when the file is fully transferred.
diff --git a/documentation/query/functions/meta.md b/documentation/query/functions/meta.md
index 535b9b21a..dae212033 100644
--- a/documentation/query/functions/meta.md
+++ b/documentation/query/functions/meta.md
@@ -285,6 +285,59 @@ Edit `server.conf` and run `reload_config`:
SELECT reload_config();
```
+## storage_policies
+
+:::note
+
+Storage policies — and the `storage_policies` view — are available in
+**QuestDB Enterprise** only.
+
+:::
+
+`storage_policies` is a system view that lists every
+[storage policy](/docs/concepts/storage-policy/) currently attached to a table
+or materialized view. Query it like any other table:
+
+```questdb-sql
+SELECT * FROM storage_policies;
+```
+
+**Columns:**
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `table_dir_name` | _STRING_ | Directory name of the table or materialized view the policy is attached to. Matches the `table_dir_name` column in [`tables()`](#tables) / [`materialized_views`](#materialized_views). |
+| `to_parquet` | _STRING_ | TTL for the `TO PARQUET` stage (e.g. `72h`, `1m`). Blank when the stage is not configured. |
+| `drop_native` | _STRING_ | TTL for the `DROP NATIVE` stage. Blank when the stage is not configured. |
+| `drop_local` | _STRING_ | TTL for the `DROP LOCAL` stage. Blank when the stage is not configured. |
+| `drop_remote` | _STRING_ | Reserved — always blank in the current release. The `DROP REMOTE` clause is rejected at SQL parse time with `'DROP REMOTE' is not supported yet`. The column is kept for forward compatibility. |
+| `status` | _CHAR_ | Policy status. `A` = active (the policy is being enforced), `D` = disabled (via [`ALTER TABLE DISABLE STORAGE POLICY`](/docs/query/sql/alter-table-set-storage-policy/)). |
+| `last_updated` | _TIMESTAMP_ | Timestamp of the most recent change to the policy definition (not the last time partitions were processed). |
+
+**Notes on TTL formatting:**
+
+- TTL values are rendered in just two units: `h` for hours and `m` for
+ **months**. Durations written in the DDL as days, weeks, or years are
+ normalized to hours when stored (e.g., `3 DAYS` → `72h`, `1 WEEK` →
+ `168h`). Month-based durations are stored and rendered with the lowercase
+ `m` suffix — despite the visual collision with "minute", `m` in this view
+ is **months**, and QuestDB's duration shorthand has no unit for minutes.
+
+**Example:**
+
+```questdb-sql title="List all storage policies"
+SELECT * FROM storage_policies;
+```
+
+| table_dir_name | to_parquet | drop_native | drop_local | drop_remote | status | last_updated |
+|----------------|------------|-------------|------------|-------------|--------|--------------|
+| trades~12 | 72h | 240h | 1m | | A | 2025-01-15T10:30:00.000000Z |
+| metrics~18 | 168h | | | | D | 2025-01-14T09:15:42.000000Z |
+
+The first row is a policy with three active stages (3-day Parquet conversion,
+10-day native drop, 1-month local drop) and is currently enforced. The second
+row has only the `TO PARQUET` stage set and has been temporarily disabled.
+
## table_columns
`table_columns('tableName')` returns the schema of a table or a materialized
@@ -385,6 +438,19 @@ Returns a table with the following columns:
partition will contain the `.detached` extension)
- `attachable` - _BOOLEAN_, true if the partition is detached and can be
attached (`name` of the partition will contain the `.attachable` extension)
+- `hasParquetGenerated` - _BOOLEAN_, true if a Parquet copy of the partition
+ has been produced alongside the native files. Set by either
+ [manual Parquet conversion](/docs/query/export-parquet/#in-place-conversion)
+ (`ALTER TABLE ... CONVERT PARTITION TO PARQUET`) or by a
+ [storage policy](/docs/concepts/storage-policy/)'s `TO PARQUET` stage
+ (Enterprise). The partition is still served from native storage until it is
+ switched to Parquet-only format
+- `isParquet` - _BOOLEAN_, true if the partition is stored in Parquet format
+ (native files have been replaced). Set the same way as
+ `hasParquetGenerated` — either manually or by a storage policy's `DROP
+ NATIVE` stage
+- `parquetFileSize` - _LONG_, size in bytes of the partition's `data.parquet`
+ file when `hasParquetGenerated` or `isParquet` is true; `-1` otherwise
**Examples:**
@@ -403,12 +469,12 @@ CREATE TABLE my_table AS (
table_partitions('my_table');
```
-| index | partitionBy | name | minTimestamp | maxTimestamp | numRows | diskSize | diskSizeHuman | readOnly | active | attached | detached | attachable |
-| ----- | ----------- | -------- | --------------------- | --------------------- | ------- | -------- | ------------- | -------- | ------ | -------- | -------- | ---------- |
-| 0 | WEEK | 2022-W52 | 2023-01-01 00:36:00.0 | 2023-01-01 23:24:00.0 | 39 | 98304 | 96.0 KiB | false | false | true | false | false |
-| 1 | WEEK | 2023-W01 | 2023-01-02 00:00:00.0 | 2023-01-08 23:24:00.0 | 280 | 98304 | 96.0 KiB | false | false | true | false | false |
-| 2 | WEEK | 2023-W02 | 2023-01-09 00:00:00.0 | 2023-01-15 23:24:00.0 | 280 | 98304 | 96.0 KiB | false | false | true | false | false |
-| 3 | WEEK | 2023-W03 | 2023-01-16 00:00:00.0 | 2023-01-18 12:00:00.0 | 101 | 83902464 | 80.0 MiB | false | true | true | false | false |
+| index | partitionBy | name | minTimestamp | maxTimestamp | numRows | diskSize | diskSizeHuman | readOnly | active | attached | detached | attachable | hasParquetGenerated | isParquet | parquetFileSize |
+| ----- | ----------- | -------- | --------------------- | --------------------- | ------- | -------- | ------------- | -------- | ------ | -------- | -------- | ---------- | ------------------- | --------- | --------------- |
+| 0 | WEEK | 2022-W52 | 2023-01-01 00:36:00.0 | 2023-01-01 23:24:00.0 | 39 | 98304 | 96.0 KiB | false | false | true | false | false | false | false | -1 |
+| 1 | WEEK | 2023-W01 | 2023-01-02 00:00:00.0 | 2023-01-08 23:24:00.0 | 280 | 98304 | 96.0 KiB | false | false | true | false | false | false | false | -1 |
+| 2 | WEEK | 2023-W02 | 2023-01-09 00:00:00.0 | 2023-01-15 23:24:00.0 | 280 | 98304 | 96.0 KiB | false | false | true | false | false | false | false | -1 |
+| 3 | WEEK | 2023-W03 | 2023-01-16 00:00:00.0 | 2023-01-18 12:00:00.0 | 101 | 83902464 | 80.0 MiB | false | true | true | false | false | false | false | -1 |
```questdb-sql title="Get size of a table in disk"
SELECT size_pretty(sum(diskSize)) FROM table_partitions('my_table');
@@ -422,9 +488,9 @@ SELECT size_pretty(sum(diskSize)) FROM table_partitions('my_table');
SELECT * FROM table_partitions('my_table') WHERE active = true;
```
-| index | partitionBy | name | minTimestamp | maxTimestamp | numRows | diskSize | diskSizeHuman | readOnly | active | attached | detached | attachable |
-| ----- | ----------- | -------- | --------------------- | --------------------- | ------- | -------- | ------------- | -------- | ------ | -------- | -------- | ---------- |
-| 3 | WEEK | 2023-W03 | 2023-01-16 00:00:00.0 | 2023-01-18 12:00:00.0 | 101 | 83902464 | 80.0 MiB | false | true | true | false | false |
+| index | partitionBy | name | minTimestamp | maxTimestamp | numRows | diskSize | diskSizeHuman | readOnly | active | attached | detached | attachable | hasParquetGenerated | isParquet | parquetFileSize |
+| ----- | ----------- | -------- | --------------------- | --------------------- | ------- | -------- | ------------- | -------- | ------ | -------- | -------- | ---------- | ------------------- | --------- | --------------- |
+| 3 | WEEK | 2023-W03 | 2023-01-16 00:00:00.0 | 2023-01-18 12:00:00.0 | 101 | 83902464 | 80.0 MiB | false | true | true | false | false | false | false | -1 |
## table_storage
diff --git a/documentation/query/sql/alter-mat-view-set-storage-policy.md b/documentation/query/sql/alter-mat-view-set-storage-policy.md
new file mode 100644
index 000000000..8628d23a1
--- /dev/null
+++ b/documentation/query/sql/alter-mat-view-set-storage-policy.md
@@ -0,0 +1,162 @@
+---
+title: ALTER MATERIALIZED VIEW SET STORAGE POLICY
+sidebar_label: SET STORAGE POLICY
+description:
+ ALTER MATERIALIZED VIEW SET STORAGE POLICY SQL keyword reference documentation.
+---
+
+Sets, modifies, enables, disables, or removes a storage policy on a materialized
+view.
+
+:::note
+
+Storage policies are available in **QuestDB Enterprise** only.
+
+:::
+
+Refer to the [Storage Policy](/docs/concepts/storage-policy/) concept guide for
+a full overview.
+
+## Syntax
+
+### Set or modify a storage policy
+
+```questdb-sql
+ALTER MATERIALIZED VIEW view_name SET STORAGE POLICY(
+ [TO PARQUET ttl,]
+ [DROP NATIVE ttl,]
+ [DROP LOCAL ttl,]
+ [DROP REMOTE ttl]
+);
+```
+
+Only the specified settings are changed. Omitted settings retain their current
+values.
+
+### Enable or disable a storage policy
+
+```questdb-sql
+ALTER MATERIALIZED VIEW view_name ENABLE STORAGE POLICY;
+ALTER MATERIALIZED VIEW view_name DISABLE STORAGE POLICY;
+```
+
+Disabling a policy suspends processing without removing the policy definition.
+
+### Remove a storage policy
+
+```questdb-sql
+ALTER MATERIALIZED VIEW view_name DROP STORAGE POLICY;
+```
+
+This permanently removes the storage policy from the materialized view.
+
+## Description
+
+A storage policy defines up to four TTL-based stages that control how partitions
+transition from native format to Parquet and eventually get removed:
+
+| Setting | Effect |
+|---------|--------|
+| `TO PARQUET ` | Convert partition from native format to Parquet locally |
+| `DROP NATIVE ` | Remove native binary files, keeping only the local Parquet copy |
+| `DROP LOCAL ` | Remove all local copies of the partition |
+| `DROP REMOTE ` | _Reserved._ Will remove the partition from object storage when remote upload is supported |
+
+:::info
+
+`DROP REMOTE` is reserved syntax. It is rejected at SQL parse time with
+`'DROP REMOTE' is not supported yet`. Automatic upload of Parquet files to
+object storage is not currently supported — storage policies operate locally
+only. Because the clause cannot take effect, the `drop_remote` column in the
+[`storage_policies`](/docs/query/functions/meta/#storage_policies) view is
+always blank in the current release.
+
+:::
+
+### TTL format
+
+Follow each setting with a duration value using one of these formats:
+
+- Long form: `3 DAYS`, `1 MONTH`, `2 YEARS`
+- Short form: `3d`, `1M`, `2Y`
+
+Supported units: `HOUR`/`h`, `DAY`/`d`, `WEEK`/`W`, `MONTH`/`M`, `YEAR`/`Y`.
+Both singular and plural forms are accepted.
+
+### Constraints
+
+- TTL values must be in ascending order:
+ `TO PARQUET <= DROP NATIVE <= DROP LOCAL <= DROP REMOTE`
+- All TTL values must be positive — `0` is rejected
+- Each setting can only appear once per statement
+- The materialized view must have a designated timestamp and partitioning enabled
+- If the materialized view has a TTL set, clear it with
+ `ALTER MATERIALIZED VIEW SET TTL 0` before setting a storage policy. Any
+ non-zero `SET TTL` value is rejected in Enterprise with
+ `TTL settings are deprecated, please, create a storage policy instead`
+- `ENABLE` and `DISABLE` require a policy to exist on the view; both return an
+ error otherwise
+
+### Permissions
+
+Each operation requires a specific permission:
+
+| SQL command | Required permission |
+|-------------|-------------------|
+| `SET STORAGE POLICY` | `SET STORAGE POLICY` |
+| `DROP STORAGE POLICY` | `REMOVE STORAGE POLICY` |
+| `ENABLE STORAGE POLICY` | `ENABLE STORAGE POLICY` |
+| `DISABLE STORAGE POLICY` | `DISABLE STORAGE POLICY` |
+
+## Examples
+
+Set a storage policy with multiple stages:
+
+```questdb-sql
+ALTER MATERIALIZED VIEW trades_hourly SET STORAGE POLICY(
+ TO PARQUET 7 DAYS,
+ DROP NATIVE 14 DAYS,
+ DROP LOCAL 1 MONTH
+);
+```
+
+Update only the Parquet conversion threshold:
+
+```questdb-sql
+ALTER MATERIALIZED VIEW trades_hourly SET STORAGE POLICY(TO PARQUET 14d);
+```
+
+Temporarily suspend a policy:
+
+```questdb-sql
+ALTER MATERIALIZED VIEW trades_hourly DISABLE STORAGE POLICY;
+```
+
+Re-enable it:
+
+```questdb-sql
+ALTER MATERIALIZED VIEW trades_hourly ENABLE STORAGE POLICY;
+```
+
+Remove a policy entirely:
+
+```questdb-sql
+ALTER MATERIALIZED VIEW trades_hourly DROP STORAGE POLICY;
+```
+
+Check active policies:
+
+```questdb-sql
+SELECT * FROM storage_policies;
+```
+
+## See also
+
+- [Storage Policy concept](/docs/concepts/storage-policy/)
+- [ALTER TABLE SET STORAGE POLICY](/docs/query/sql/alter-table-set-storage-policy/)
+- [CREATE MATERIALIZED VIEW](/docs/query/sql/create-mat-view/)
+- [ALTER MATERIALIZED VIEW SET TTL](/docs/query/sql/alter-mat-view-set-ttl/)
+- [`storage_policies`](/docs/query/functions/meta/#storage_policies) — system
+ view listing active policies
+- [RBAC permissions](/docs/security/rbac/#permissions) — `SET`, `REMOVE`,
+ `ENABLE`, and `DISABLE STORAGE POLICY` permissions
diff --git a/documentation/query/sql/alter-mat-view-set-ttl.md b/documentation/query/sql/alter-mat-view-set-ttl.md
index 6a103788a..143eff9ed 100644
--- a/documentation/query/sql/alter-mat-view-set-ttl.md
+++ b/documentation/query/sql/alter-mat-view-set-ttl.md
@@ -8,6 +8,18 @@ description:
Sets the [time-to-live](/docs/concepts/ttl/) (TTL) period on a materialized
view, automatically dropping partitions older than the specified duration.
+:::caution
+
+**QuestDB Enterprise: TTL is deprecated.** Enterprise rejects any non-zero
+`SET TTL` with
+`TTL settings are deprecated, please, create a storage policy instead`. Use
+[`ALTER MATERIALIZED VIEW SET STORAGE POLICY`](/docs/query/sql/alter-mat-view-set-storage-policy/)
+instead. `SET TTL 0` is still accepted, for clearing a pre-existing TTL before
+attaching a storage policy. See [Storage Policy](/docs/concepts/storage-policy/)
+for the Enterprise replacement.
+
+:::
+
## Syntax
```
diff --git a/documentation/query/sql/alter-table-set-storage-policy.md b/documentation/query/sql/alter-table-set-storage-policy.md
new file mode 100644
index 000000000..32e7571f1
--- /dev/null
+++ b/documentation/query/sql/alter-table-set-storage-policy.md
@@ -0,0 +1,182 @@
+---
+title: ALTER TABLE SET STORAGE POLICY
+sidebar_label: SET STORAGE POLICY
+description: ALTER TABLE SET STORAGE POLICY SQL keyword reference documentation.
+---
+
+Sets, modifies, enables, disables, or removes a storage policy on a table. For
+the equivalent operations on materialized views, see
+[ALTER MATERIALIZED VIEW SET STORAGE POLICY](/docs/query/sql/alter-mat-view-set-storage-policy/).
+
+:::note
+
+Storage policies are available in **QuestDB Enterprise** only.
+
+:::
+
+Refer to the [Storage Policy](/docs/concepts/storage-policy/) concept guide for
+a full overview.
+
+## Syntax
+
+### Set or modify a storage policy
+
+```questdb-sql
+ALTER TABLE table_name SET STORAGE POLICY(
+ [TO PARQUET ttl,]
+ [DROP NATIVE ttl,]
+ [DROP LOCAL ttl,]
+ [DROP REMOTE ttl]
+);
+```
+
+Only the specified settings are changed. Omitted settings retain their current
+values.
+
+### Enable or disable a storage policy
+
+```questdb-sql
+ALTER TABLE table_name ENABLE STORAGE POLICY;
+ALTER TABLE table_name DISABLE STORAGE POLICY;
+```
+
+Disabling a policy suspends processing without removing the policy definition.
+
+### Remove a storage policy
+
+```questdb-sql
+ALTER TABLE table_name DROP STORAGE POLICY;
+```
+
+This permanently removes the storage policy from the table.
+
+## Description
+
+A storage policy defines up to four TTL-based stages that control how partitions
+transition from native format to Parquet and eventually get removed:
+
+| Setting | Effect |
+|---------|--------|
+| `TO PARQUET ` | Convert partition from native format to Parquet locally |
+| `DROP NATIVE ` | Remove native binary files, keeping only the local Parquet copy |
+| `DROP LOCAL ` | Remove all local copies of the partition |
+| `DROP REMOTE ` | _Reserved._ Will remove the partition from object storage when remote upload is supported |
+
+:::info
+
+`DROP REMOTE` is reserved syntax. It is rejected at SQL parse time with
+`'DROP REMOTE' is not supported yet`. Automatic upload of Parquet files to
+object storage is not currently supported — storage policies operate locally
+only. Because the clause cannot take effect, the `drop_remote` column in the
+[`storage_policies`](/docs/query/functions/meta/#storage_policies) view is
+always blank in the current release.
+
+:::
+
+### TTL format
+
+Follow each setting with a duration value using one of these formats:
+
+- Long form: `3 DAYS`, `1 MONTH`, `2 YEARS`
+- Short form: `3d`, `1M`, `2Y`
+
+Supported units: `HOUR`/`h`, `DAY`/`d`, `WEEK`/`W`, `MONTH`/`M`, `YEAR`/`Y`.
+Both singular and plural forms are accepted.
+
+### Constraints
+
+- TTL values must be in ascending order:
+ `TO PARQUET <= DROP NATIVE <= DROP LOCAL <= DROP REMOTE`
+- All TTL values must be positive — `0` is rejected
+- Each setting can only appear once per statement
+- The table must have a designated timestamp and partitioning enabled
+- If the table has a TTL set, clear it with `ALTER TABLE SET TTL 0` before
+ setting a storage policy. Any non-zero `SET TTL` value is rejected in
+ Enterprise with `TTL settings are deprecated, please, create a storage policy
+ instead`
+- `ENABLE` and `DISABLE` require a policy to exist on the table; both return an
+ error otherwise
+
+### Permissions
+
+Each operation requires a specific permission:
+
+| SQL command | Required permission |
+|-------------|-------------------|
+| `SET STORAGE POLICY` | `SET STORAGE POLICY` |
+| `DROP STORAGE POLICY` | `REMOVE STORAGE POLICY` |
+| `ENABLE STORAGE POLICY` | `ENABLE STORAGE POLICY` |
+| `DISABLE STORAGE POLICY` | `DISABLE STORAGE POLICY` |
+
+## Examples
+
+Set a storage policy with all three currently-supported stages:
+
+```questdb-sql
+ALTER TABLE sensor_data SET STORAGE POLICY(
+ TO PARQUET 3 DAYS,
+ DROP NATIVE 10 DAYS,
+ DROP LOCAL 1 MONTH
+);
+```
+
+Update only the Parquet conversion threshold:
+
+```questdb-sql
+ALTER TABLE sensor_data SET STORAGE POLICY(TO PARQUET 7d);
+```
+
+Temporarily suspend a policy:
+
+```questdb-sql
+ALTER TABLE sensor_data DISABLE STORAGE POLICY;
+```
+
+Re-enable it:
+
+```questdb-sql
+ALTER TABLE sensor_data ENABLE STORAGE POLICY;
+```
+
+Remove a policy entirely:
+
+```questdb-sql
+ALTER TABLE sensor_data DROP STORAGE POLICY;
+```
+
+Check active policies:
+
+```questdb-sql
+SELECT * FROM storage_policies;
+```
+
+The storage policy also appears in `SHOW CREATE TABLE` output:
+
+```questdb-sql
+SHOW CREATE TABLE sensor_data;
+```
+
+```text
+CREATE TABLE 'sensor_data' (
+ ts TIMESTAMP,
+ value DOUBLE
+) timestamp(ts) PARTITION BY DAY
+STORAGE POLICY(TO PARQUET 3 DAYS, DROP NATIVE 10 DAYS, DROP LOCAL 1 MONTH) WAL;
+```
+
+Stages that are not set are omitted from the output.
+
+## See also
+
+- [Storage Policy concept](/docs/concepts/storage-policy/)
+- [ALTER MATERIALIZED VIEW SET STORAGE POLICY](/docs/query/sql/alter-mat-view-set-storage-policy/)
+- [CREATE TABLE](/docs/query/sql/create-table/) — `STORAGE POLICY` clause at
+ table creation
+- [ALTER TABLE SET TTL](/docs/query/sql/alter-table-set-ttl/) — the TTL
+ feature storage policies supersede in Enterprise
+- [`storage_policies`](/docs/query/functions/meta/#storage_policies) — system
+ view listing active policies
+- [`SHOW CREATE TABLE`](/docs/query/sql/show/#show-create-table) — displays
+ the attached `STORAGE POLICY` clause
+- [RBAC permissions](/docs/security/rbac/#permissions) — `SET`, `REMOVE`,
+ `ENABLE`, and `DISABLE STORAGE POLICY` permissions
diff --git a/documentation/query/sql/alter-table-set-ttl.md b/documentation/query/sql/alter-table-set-ttl.md
index 70bb861e8..06b8d4309 100644
--- a/documentation/query/sql/alter-table-set-ttl.md
+++ b/documentation/query/sql/alter-table-set-ttl.md
@@ -6,6 +6,18 @@ description: ALTER TABLE SET TTL SQL keyword reference documentation.
Sets the time-to-live (TTL) period on a table.
+:::caution
+
+**QuestDB Enterprise: TTL is deprecated.** Enterprise rejects any non-zero
+`SET TTL` with
+`TTL settings are deprecated, please, create a storage policy instead`. Use
+[`ALTER TABLE SET STORAGE POLICY`](/docs/query/sql/alter-table-set-storage-policy/)
+instead. `SET TTL 0` is still accepted, for clearing a pre-existing TTL before
+attaching a storage policy. See [Storage Policy](/docs/concepts/storage-policy/)
+for the Enterprise replacement.
+
+:::
+
Refer to the [section on TTL](/docs/concepts/ttl/) for a conceptual overview.
## Syntax
diff --git a/documentation/query/sql/create-mat-view.md b/documentation/query/sql/create-mat-view.md
index a3e7fc378..4d1444671 100644
--- a/documentation/query/sql/create-mat-view.md
+++ b/documentation/query/sql/create-mat-view.md
@@ -20,13 +20,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] viewName
[ PERIOD ( SAMPLE BY INTERVAL ) ] ]
AS [ ( ] query [ ) ]
[ TIMESTAMP ( columnRef ) ]
-[ PARTITION BY ( YEAR | MONTH | WEEK | DAY | HOUR ) [ TTL n timeUnit ] ]
+[ PARTITION BY ( YEAR | MONTH | WEEK | DAY | HOUR )
+ [ TTL n timeUnit
+ | STORAGE POLICY ( policyStage [, policyStage ...] ) ] ]
[ OWNED BY ownerName ]
```
Where:
- `interval`: Duration like `1m`, `10m`, `1h`, `1d`
- `timeUnit`: `HOURS | DAYS | WEEKS | MONTHS | YEARS`
+- `policyStage`: `TO PARQUET duration | DROP NATIVE duration | DROP LOCAL duration | DROP REMOTE duration`
+ (Enterprise only; all stages optional; durations must be positive and in ascending order)
- `query`: Must contain `SAMPLE BY` or time-based `GROUP BY`
## Parameters
@@ -42,6 +46,7 @@ Where:
| `TIMESTAMP` | Designate timestamp column for the view |
| `PARTITION BY` | Partitioning unit for view storage |
| `TTL` | Retention period for view data |
+| `STORAGE POLICY` | Partition lifecycle automation (Enterprise) — mutually exclusive with `TTL` |
| `OWNED BY` | Assign ownership (Enterprise) |
## Rules and defaults
@@ -277,6 +282,45 @@ Time units: `HOURS`, `DAYS`, `WEEKS`, `MONTHS`, `YEARS`
The view's TTL is independent of the base table's TTL. See
[TTL documentation](/docs/concepts/ttl/) for details.
+:::note
+
+In QuestDB Enterprise, `TTL` is deprecated — `CREATE MATERIALIZED VIEW ... TTL`
+is rejected with `TTL settings are deprecated, please, create a storage policy
+instead`. Use `STORAGE POLICY` instead. If a legacy materialized view has a TTL
+set, clear it with `ALTER MATERIALIZED VIEW SET TTL 0` before setting a storage
+policy.
+
+:::
+
+## Storage Policy
+
+:::note
+
+Storage policies are available in **QuestDB Enterprise** only.
+
+:::
+
+A [storage policy](/docs/concepts/storage-policy/) automates the partition
+lifecycle by defining when partitions are converted to Parquet locally, when
+native data is removed, and when local copies are dropped. Place the
+`STORAGE POLICY(...)` clause after `PARTITION BY`:
+
+```questdb-sql title="With storage policy (Enterprise)"
+CREATE MATERIALIZED VIEW trades_hourly AS (
+ SELECT timestamp, symbol, avg(price) AS avg_price FROM trades SAMPLE BY 1h
+) PARTITION BY DAY
+ STORAGE POLICY(TO PARQUET 7d, DROP NATIVE 14d);
+```
+
+A storage policy supports up to four settings: `TO PARQUET`, `DROP NATIVE`,
+`DROP LOCAL`, and `DROP REMOTE`. All are optional, all TTL values must be
+positive, and they must be in ascending order. `DROP REMOTE` is reserved
+syntax and is currently rejected at SQL parse time with
+`'DROP REMOTE' is not supported yet`.
+
+To modify a storage policy after creation, see
+[ALTER MATERIALIZED VIEW SET STORAGE POLICY](/docs/query/sql/alter-mat-view-set-storage-policy/).
+
## Complete example
Putting it all together:
@@ -390,6 +434,7 @@ GRANT DROP MATERIALIZED VIEW ON trades_hourly TO user1;
| `base table does not exist` | Referenced table doesn't exist |
| `query is not supported` | Query doesn't meet constraints (missing SAMPLE BY, uses FILL, etc.) |
| `permission denied` | Missing required permission (Enterprise) |
+| `TTL settings are deprecated, please, create a storage policy instead` | `TTL` clause used in QuestDB Enterprise — use `STORAGE POLICY` instead |
## See also
@@ -397,4 +442,5 @@ GRANT DROP MATERIALIZED VIEW ON trades_hourly TO user1;
- [REFRESH MATERIALIZED VIEW](/docs/query/sql/refresh-mat-view/)
- [DROP MATERIALIZED VIEW](/docs/query/sql/drop-mat-view/)
- [ALTER MATERIALIZED VIEW SET REFRESH](/docs/query/sql/alter-mat-view-set-refresh/)
+- [ALTER MATERIALIZED VIEW SET STORAGE POLICY](/docs/query/sql/alter-mat-view-set-storage-policy/)
- [ALTER MATERIALIZED VIEW SET TTL](/docs/query/sql/alter-mat-view-set-ttl/)
diff --git a/documentation/query/sql/create-table.md b/documentation/query/sql/create-table.md
index 45542f777..d54074ce5 100644
--- a/documentation/query/sql/create-table.md
+++ b/documentation/query/sql/create-table.md
@@ -21,6 +21,7 @@ The first two modes accept the same set of optional clauses:
- [`TIMESTAMP`](#designated-timestamp) - designated timestamp column
- [`PARTITION BY`](#partitioning) - partition unit and WAL mode
- [`TTL`](#time-to-live-ttl) - time-to-live for partitions
+- [`STORAGE POLICY`](#storage-policy) - partition lifecycle automation (Enterprise)
- [`DEDUP`](#deduplication) - deduplication keys (can also be set later with
[`ALTER TABLE DEDUP ENABLE`](/docs/query/sql/alter-table-enable-deduplication/))
- [`WITH`](#with-table-parameter) - table parameters
@@ -36,7 +37,8 @@ TABLE [IF NOT EXISTS] tableName
[TIMESTAMP (columnName)
[PARTITION BY { NONE | YEAR | MONTH | DAY | HOUR }
[BYPASS WAL | WAL]
- [TTL n { HOUR[S] | DAY[S] | WEEK[S] | MONTH[S] | YEAR[S] }]]]
+ [ TTL n { HOUR[S] | DAY[S] | WEEK[S] | MONTH[S] | YEAR[S] }
+ | STORAGE POLICY ( policyStage [, policyStage ...] ) ]]]
[DEDUP UPSERT KEYS (columnName [, columnName ...])]
[WITH tableParameter]
[IN VOLUME 'alias']
@@ -52,13 +54,24 @@ TABLE [IF NOT EXISTS] tableName
[TIMESTAMP (columnName)
[PARTITION BY { NONE | YEAR | MONTH | DAY | HOUR }
[BYPASS WAL | WAL]
- [TTL n { HOUR[S] | DAY[S] | WEEK[S] | MONTH[S] | YEAR[S] }]]]
+ [ TTL n { HOUR[S] | DAY[S] | WEEK[S] | MONTH[S] | YEAR[S] }
+ | STORAGE POLICY ( policyStage [, policyStage ...] ) ]]]
[DEDUP UPSERT KEYS (columnName [, columnName ...])]
[WITH tableParameter]
[IN VOLUME 'alias']
[OWNED BY ownerName];
```
+Where `policyStage` is one of:
+
+```
+TO PARQUET duration | DROP NATIVE duration | DROP LOCAL duration | DROP REMOTE duration
+```
+
+Stages are Enterprise-only, all optional, and their durations must be positive
+and in ascending order. `TTL` and `STORAGE POLICY` are mutually exclusive — see
+[Storage Policy](#storage-policy).
+
```questdb-sql title="Create from another table's structure (CREATE TABLE LIKE)"
CREATE TABLE tableName (LIKE sourceTableName);
```
@@ -241,6 +254,49 @@ information on the behavior of this feature.
:::
+:::note
+
+In QuestDB Enterprise, `TTL` is deprecated — `CREATE TABLE ... TTL` is
+rejected with `TTL settings are deprecated, please, create a storage policy
+instead`. Use `STORAGE POLICY` instead. If a legacy table has a TTL set, clear
+it with `ALTER TABLE SET TTL 0` before setting a storage policy.
+
+:::
+
+## Storage Policy
+
+:::note
+
+Storage policies are available in **QuestDB Enterprise** only.
+
+:::
+
+A [storage policy](/docs/concepts/storage-policy/) automates the partition
+lifecycle by defining when partitions are converted to Parquet locally, when
+native data is removed, and when local copies are dropped. Place the
+`STORAGE POLICY(...)` clause after `PARTITION BY`:
+
+```questdb-sql title="With storage policy (Enterprise)"
+CREATE TABLE trades (
+ timestamp TIMESTAMP,
+ symbol SYMBOL,
+ price DOUBLE,
+ amount DOUBLE
+) TIMESTAMP(timestamp)
+PARTITION BY DAY
+STORAGE POLICY(TO PARQUET 3d, DROP NATIVE 10d, DROP LOCAL 1M)
+WAL;
+```
+
+A storage policy supports up to four settings: `TO PARQUET`, `DROP NATIVE`,
+`DROP LOCAL`, and `DROP REMOTE`. All are optional, all TTL values must be
+positive, and they must be in ascending order. `DROP REMOTE` is reserved
+syntax and is currently rejected at SQL parse time with
+`'DROP REMOTE' is not supported yet`.
+
+To modify a storage policy after table creation, see
+[ALTER TABLE SET STORAGE POLICY](/docs/query/sql/alter-table-set-storage-policy/).
+
## Deduplication
When [Deduplication](/docs/concepts/deduplication) is enabled, QuestDB only
diff --git a/documentation/query/sql/show.md b/documentation/query/sql/show.md
index 0d94883dd..6976ce129 100644
--- a/documentation/query/sql/show.md
+++ b/documentation/query/sql/show.md
@@ -117,6 +117,29 @@ CREATE TABLE sensors (
) timestamp(ts) PARTITION BY DAY BYPASS WAL;
```
+#### Storage policy clause
+
+When a [storage policy](/docs/concepts/storage-policy/) is attached to a table
+(Enterprise only), the policy renders as a `STORAGE POLICY(...)` clause in the
+`SHOW CREATE TABLE` output:
+
+```questdb-sql
+SHOW CREATE TABLE sensor_data;
+```
+
+```text
+CREATE TABLE 'sensor_data' (
+ ts TIMESTAMP,
+ value DOUBLE
+) timestamp(ts) PARTITION BY DAY
+STORAGE POLICY(TO PARQUET 3 DAYS, DROP NATIVE 10 DAYS, DROP LOCAL 1 MONTH) WAL;
+```
+
+Stages that are not configured on the policy are omitted from the clause. A
+disabled policy (`ALTER TABLE ... DISABLE STORAGE POLICY`) still renders — the
+disabled state is not part of the DDL. See
+[ALTER TABLE SET STORAGE POLICY](/docs/query/sql/alter-table-set-storage-policy/).
+
#### Enterprise variant
[QuestDB Enterprise](/enterprise/) will include an additional `OWNED BY` clause populated with the current user.
@@ -159,12 +182,16 @@ including any `DECLARE` parameters if the view is parameterized.
SHOW PARTITIONS FROM my_table;
```
-| index | partitionBy | name | minTimestamp | maxTimestamp | numRows | diskSize | diskSizeHuman | readOnly | active | attached | detached | attachable |
-| ----- | ----------- | -------- | --------------------- | --------------------- | ------- | -------- | ------------- | -------- | ------ | -------- | -------- | ---------- |
-| 0 | WEEK | 2022-W52 | 2023-01-01 00:36:00.0 | 2023-01-01 23:24:00.0 | 39 | 98304 | 96.0 KiB | false | false | true | false | false |
-| 1 | WEEK | 2023-W01 | 2023-01-02 00:00:00.0 | 2023-01-08 23:24:00.0 | 280 | 98304 | 96.0 KiB | false | false | true | false | false |
-| 2 | WEEK | 2023-W02 | 2023-01-09 00:00:00.0 | 2023-01-15 23:24:00.0 | 280 | 98304 | 96.0 KiB | false | false | true | false | false |
-| 3 | WEEK | 2023-W03 | 2023-01-16 00:00:00.0 | 2023-01-18 12:00:00.0 | 101 | 83902464 | 80.0 MiB | false | true | true | false | false |
+| index | partitionBy | name | minTimestamp | maxTimestamp | numRows | diskSize | diskSizeHuman | readOnly | active | attached | detached | attachable | hasParquetGenerated | isParquet | parquetFileSize |
+| ----- | ----------- | -------- | --------------------- | --------------------- | ------- | -------- | ------------- | -------- | ------ | -------- | -------- | ---------- | ------------------- | --------- | --------------- |
+| 0 | WEEK | 2022-W52 | 2023-01-01 00:36:00.0 | 2023-01-01 23:24:00.0 | 39 | 98304 | 96.0 KiB | false | false | true | false | false | false | false | -1 |
+| 1 | WEEK | 2023-W01 | 2023-01-02 00:00:00.0 | 2023-01-08 23:24:00.0 | 280 | 98304 | 96.0 KiB | false | false | true | false | false | false | false | -1 |
+| 2 | WEEK | 2023-W02 | 2023-01-09 00:00:00.0 | 2023-01-15 23:24:00.0 | 280 | 98304 | 96.0 KiB | false | false | true | false | false | false | false | -1 |
+| 3 | WEEK | 2023-W03 | 2023-01-16 00:00:00.0 | 2023-01-18 12:00:00.0 | 101 | 83902464 | 80.0 MiB | false | true | true | false | false | false | false | -1 |
+
+See [`table_partitions()`](/docs/query/functions/meta/#table_partitions) for the
+full column list, including `hasParquetGenerated`, `isParquet`, and
+`parquetFileSize`.
### SHOW PARAMETERS
diff --git a/documentation/security/rbac.md b/documentation/security/rbac.md
index 09101e276..b82e3eff7 100644
--- a/documentation/security/rbac.md
+++ b/documentation/security/rbac.md
@@ -571,18 +571,22 @@ SELECT * FROM all_permissions();
| DEDUP ENABLE | Database | Table | Enable deduplication |
| DEDUP DISABLE | Database | Table | Disable deduplication |
| DETACH PARTITION | Database | Table | Detach partitions |
+| DISABLE STORAGE POLICY | Database | Table | Disable storage policies |
| DROP COLUMN | Database | Table | Column | Drop columns |
| DROP INDEX | Database | Table | Column | Drop indexes |
| DROP PARTITION | Database | Table | Drop partitions |
| DROP TABLE | Database | Table | Drop tables |
| DROP MATERIALIZED VIEW | Database | Table | Drop materialized views |
+| ENABLE STORAGE POLICY | Database | Table | Enable storage policies |
| INSERT | Database | Table | Insert data |
| REFRESH MATERIALIZED VIEW | Database | Table | Refresh materialized views |
| REINDEX | Database | Table | Column | Reindex columns |
+| REMOVE STORAGE POLICY | Database | Table | Remove storage policies |
| RENAME COLUMN | Database | Table | Column | Rename columns |
| RENAME TABLE | Database | Table | Rename tables |
| RESUME WAL | Database | Table | Resume WAL processing |
| SELECT | Database | Table | Column | Read data |
+| SET STORAGE POLICY | Database | Table | Set storage policies |
| SET TABLE PARAM | Database | Table | Set table parameters |
| SET TABLE TYPE | Database | Table | Change table type |
| SETTINGS | Database | Change instance settings in Web Console |
diff --git a/documentation/sidebars.js b/documentation/sidebars.js
index bf61c7620..bf99c48ad 100644
--- a/documentation/sidebars.js
+++ b/documentation/sidebars.js
@@ -287,6 +287,7 @@ module.exports = {
"query/sql/alter-table-rename-column",
"query/sql/alter-table-resume-wal",
"query/sql/alter-table-set-param",
+ "query/sql/alter-table-set-storage-policy",
"query/sql/alter-table-set-ttl",
"query/sql/alter-table-set-type",
"query/sql/alter-table-squash-partitions",
@@ -308,6 +309,7 @@ module.exports = {
"query/sql/alter-mat-view-resume-wal",
"query/sql/alter-mat-view-set-refresh",
"query/sql/alter-mat-view-set-refresh-limit",
+ "query/sql/alter-mat-view-set-storage-policy",
"query/sql/alter-mat-view-set-ttl",
],
},
@@ -532,6 +534,7 @@ module.exports = {
},
"concepts/deduplication",
"concepts/ttl",
+ "concepts/storage-policy",
"concepts/write-ahead-log",
],
},