TRUNCATE PARTITION [, ...] [DROP | REUSE STORAGE] [UPDATE [GLOBAL] INDEXES] [CASCADE];
ALTER TABLE TRUNCATE SUBPARTITION ...;
ALTER TABLE DROP PARTITION [UPDATE [GLOBAL] INDEXES];
TRUNCATE CLUSTER ;
```
- `DROP STORAGE` (default) — deallocate extents beyond `MINEXTENTS`. `REUSE STORAGE` — keep them (faster, and the space is about to be reused anyway). `DROP ALL STORAGE` (11.2.0.2+) — deallocate everything including the first extent.
- `CASCADE` (12c+) — also truncate child tables whose FK to this table is `ON DELETE CASCADE`.
- `UPDATE INDEXES` on a partition truncate — keep global indexes usable (does the maintenance now, or asynchronously in 12c+); without it, global indexes become `UNUSABLE`.
## 3. Examples on the Course Schema
### 3a. Staging table reload — TRUNCATE + APPEND
```sql
TRUNCATE TABLE orders_stage REUSE STORAGE; -- ms; HWM reset, extents kept
INSERT /*+ APPEND */ INTO orders_stage SELECT ... FROM external_orders_ext;
COMMIT;
```
This pair is the standard nightly-load shape. `DELETE` + `APPEND` instead would grow the table every night ([APPEND HINT](append-hint.md) §4).
### 3b. Partition instead of DELETE
```sql
-- DELETE of 40M rows: hours, 40M undo records, 40M index deletes
DELETE FROM orders WHERE order_date >= DATE '2024-12-01' AND order_date < DATE '2025-01-01';
-- the same, as DDL: seconds
ALTER TABLE orders TRUNCATE PARTITION p_2024_12 UPDATE INDEXES;
```
Requires the table to be partitioned on `order_date` so that "December 2024" *is* a partition — this is the main reason large tables are range-partitioned by date.
### 3c. From PL/SQL — dynamic, validated, and outside the transaction
```sql
CREATE OR REPLACE PROCEDURE purge_month (p_partition VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE orders TRUNCATE PARTITION ' ||
DBMS_ASSERT.SIMPLE_SQL_NAME(p_partition) || ' UPDATE INDEXES';
END;
/
```
Call it **before** the transactional work of a job, or after its final commit — never between an `UPDATE` and its intended `COMMIT`:
```sql
BEGIN
UPDATE orders SET status = 'X' WHERE order_id = 1;
purge_month('P_2024_12'); -- commits the UPDATE silently
RAISE_APPLICATION_ERROR(-20001, 'validation failed'); -- too late: the UPDATE is permanent
END;
```
### 3d. Foreign keys
```sql
TRUNCATE TABLE customers;
-- ORA-02266: unique/primary keys in table referenced by enabled foreign keys
-- options:
ALTER TABLE orders DISABLE CONSTRAINT orders_cust_fk;
TRUNCATE TABLE customers;
ALTER TABLE orders ENABLE NOVALIDATE CONSTRAINT orders_cust_fk; -- or ENABLE VALIDATE if the data allows
-- 12c+, if orders_cust_fk is ON DELETE CASCADE:
TRUNCATE TABLE customers CASCADE; -- truncates orders too
```
`ORA-02266` is raised even if the child table is empty — the check is on the constraint's existence, not on rows.
### 3e. Global indexes on partition truncate
```sql
ALTER TABLE orders TRUNCATE PARTITION p_2024_12; -- global indexes → UNUSABLE
SELECT index_name, status FROM user_indexes WHERE table_name = 'ORDERS';
ALTER INDEX orders_cust_gix REBUILD PARALLEL 8; -- or:
ALTER TABLE orders TRUNCATE PARTITION p_2024_12 UPDATE INDEXES; -- global indexes stay VALID
```
12c+ does the global-index maintenance asynchronously: the truncate returns immediately, the index entries are marked orphaned and cleaned by the `SYS.PMO_DEFERRED_GIDX_MAINT_JOB` job (or `DBMS_PART.CLEANUP_GIDX`). Local indexes are truncated along with the partition — no maintenance needed, which is the argument for local indexes on partitioned tables.
### 3f. Locked rows
```sql
TRUNCATE TABLE orders_stage;
-- ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
```
Another session holds a row lock (an uncommitted `INSERT` into the stage table). DDL needs an exclusive table lock; it waits `DDL_LOCK_TIMEOUT` seconds (default 0 = fail immediately). `ALTER SESSION SET ddl_lock_timeout = 60` gives it a minute.
## 4. What Breaks at Scale
- `DELETE` of tens of millions of rows is the operation that fills undo, generates the most redo per useful byte, and leaves the table the same size with free space scattered inside — every later full scan still reads the empty blocks up to the HWM. `TRUNCATE` (or partition truncate/drop) is not an optimization of `DELETE`; it is the correct operation, and the table design (partitioning) should make it available.
- `DROP STORAGE` on a table with hundreds of thousands of extents can itself take a while (extent deallocation in the dictionary / bitmap). `REUSE STORAGE` when the space will be refilled.
- Materialized view logs on the table: `TRUNCATE` by default purges the MV log (`PURGE MATERIALIZED VIEW LOG`), which forces the next refresh to be complete rather than fast.
- Statistics are not touched; after a truncate-and-reload the old stats may describe a very different table. Gather after the load (or rely on 12c online stats gathering for direct-path loads into empty tables).
- Flashback Query cannot see truncated rows (no undo was generated). Recovery is RMAN / Flashback Database — which is the DBA's reminder: truncating the wrong table is a restore, not an undo.
## 5. Pitfalls
1. Implicit `COMMIT` — in the middle of a transaction, silently.
2. No `ROLLBACK`, no Flashback Query.
3. `ORA-02266` — enabled FK referencing the table, even with an empty child.
4. `ORA-00054` — DDL lock conflict with any uncommitted DML on the table; set `DDL_LOCK_TIMEOUT`.
5. Global indexes `UNUSABLE` after partition truncate without `UPDATE INDEXES` — queries fail with `ORA-01502` or fall back to full scans depending on `SKIP_UNUSABLE_INDEXES`.
6. Triggers do not fire — audit triggers miss the deletion entirely.
7. Truncating a table other schemas depend on for MV fast refresh — the MV log is purged.
8. `TRUNCATE` on a table in someone else's schema needs `DROP ANY TABLE`, not `DELETE` — a much bigger privilege. Wrap it in a definer's-rights procedure in the owning schema and grant `EXECUTE`.
## 6. Related
- [EXCHANGE PARTITION](exchange-partition.md) — the other zero-row-work partition operation.
- [APPEND HINT](append-hint.md) — the natural partner for reloads.
- [COMMIT](commit.md) — DDL and implicit commits.
- [EXECUTE IMMEDIATE](execute-immediate.md) / [DBMS_ASSERT](dbms_assert.md) — dynamic partition names.