# Redshift Storage Utilization — Attribution & Measurement Answers **what is consuming space and why**; act via `references/redshift-storage-remediation.md`. Attribute before remediating, and establish Serverless vs provisioned first (SKILL.md STEP 0) — the views and what "used" means differ, and `SELECT version()` does not tell you which one you're on. **Always name the specific system views and columns you would query, then interpret them.** A verdict with no views behind it is not actionable — state the view, the column, and what value would confirm or refute each candidate cause. **Answer shape — front-load it.** Lead with (1) the verdict, (2) the causal chain including *why* the symptom looks the way it does, (3) the remediation list — every action, in order — then (4) what you explicitly rule OUT. A reader who stops after the opening should already hold both the diagnosis and every fix; supporting queries, caveats, and background come *after* the conclusions they support, never before. **Answer the question asked, completely.** Cover every conclusion, every diagnostic step, and every remediation the question needs, with the reasoning that makes each one verifiable — never omit a step or a caveat to be brief. Stay on the question: a spike question gets the spike diagnosis, a vacuum question gets the vacuum answer; do not append an unrelated investigation the customer did not raise. Explain each mechanism where it is used rather than repeating it across sections. **Snapshot / backup billing questions: use the S16 matrix at the end of this file — do not answer from the general AWS Backup rule or from "Redshift snapshots are incremental" alone.** Billing depends on snapshot type (**automated** vs **manual**) AND platform (**RA3** vs **Serverless**/**RG**), and on RA3 each retained **manual** snapshot bills close to its own full size. ## What "used" means - **DC2/DS2 (local SSD):** real out-of-space — `STV_NODE_STORAGE_CAPACITY` used/capacity (**provisioned single-AZ only** — the `STL_`/`STV_` families are disabled on Multi-AZ and absent on Serverless, so a monitoring query built on them breaks on a single-AZ → Multi-AZ move). - **RA3/Serverless (managed storage):** local fill is **storage + cache** (plus spill), not billable capacity alone, so a high local reading is usually not out-of-space. Do not VACUUM for cache pressure. - **`PercentageDiskSpaceUsed` is not a pure data-growth signal** — query **spill** and **temp** blocks raise it too, which is why a spike can self-recover with no data change. Pair it with `SVV_TABLE_INFO.size` (logical permanent data) before concluding growth. - **On an error 1016, `STL_DISK_FULL_DIAG` is the documented first stop** — it is the one view that answers *which query filled which node*: `SELECT node_num, query_id, temp_blocks FROM stl_disk_full_diag;` (for the event time add `'2000-01-01'::timestamp + (currenttime/1000000.0) * interval '1 second' AS currenttime`). **`temp_blocks` ≈ 0 ⇒ the fill is persistent data**; non-zero ⇒ attribute it to that `query_id`'s **spill** rather than to growth or maintenance debt. Check this *before* reaching for VACUUM — it is the fastest split between the two causes. **Provisioned single-AZ only**, like the rest of the `STL_`/`STV_` family. - Physical bytes = permanent **user tables** + **temp**/spill + **system**/catalog + **ghost**/**tombstone** + **snapshot** + **datashare** blocks. Only the first is in `svv_table_info`. - **`SVV_TABLES_SPACE_USAGE` is the fastest way to split that total per node** — one row per node with `capacity`, `local_perm_tables_space_used`, `local_temp_tables_space_used` and `datasharing_tables_space_used`, which answers "is this my data, my temp/spill, or inbound shared data?" in a single query and shows *which* nodes carry the imbalance: `SELECT node, capacity, local_perm_tables_space_used, local_temp_tables_space_used, datasharing_tables_space_used FROM svv_tables_space_usage ORDER BY node;`. Requires superuser. Pair its verdict with the figures the customer can cross-check directly — `svv_table_info` for permanent size and `PercentageDiskSpaceUsed` in CloudWatch for the fill. - **If the cluster is unavailable or stuck (modifying / restarting), you can still attribute — but only from CloudWatch.** The `STL_`/`STV_` views are unreachable while it is down, so read **`PercentageDiskSpaceUsed`** there to confirm whether the fill really is near 100%, and use `stv_node_storage_capacity` once it is back. **Do not assume storage is the cause:** an active regional event, a hardware fault, a Serverless control-plane workflow, or the state of a datashare producer can each wedge a cluster whose storage is fine. Confirm the fill first, then hand off to `references/redshift-storage-remediation.md` (S12 for a stuck cluster, S18 for a failed/rolled-back resize) for the recovery playbook — the customer cannot force a stuck workflow, so recovery is adding capacity via **elastic resize** for **headroom** plus escalating to the Redshift service team, never a VACUUM first. ## Anchor query — attribute by table (both platforms) `SVV_TABLE_INFO` is superuser-gated (`GRANT SELECT ON svv_table_info TO `). ```sql SELECT "schema","table",size/1024.0 AS size_gb,tbl_rows,estimated_visible_rows, tbl_rows-estimated_visible_rows AS ghost_rows,unsorted AS pct_unsorted, vacuum_sort_benefit,stats_off,skew_rows,diststyle FROM svv_table_info ORDER BY size DESC LIMIT ; ``` Growth vs debt: `size`+`tbl_rows` up = real growth; large `ghost_rows` = VACUUM DELETE ONLY debt; `pct_unsorted` high + `vacuum_sort_benefit`>0 = sort debt, ≈0 = do-not-vacuum trap; `skew_rows` high = distribution debt (remediate with **EVEN** diststyle or a better distribution key); `stats_off` high = ANALYZE. `ghost_rows` is a **raw count**, not a ratio — always judge it *against* `tbl_rows` before acting (14 ghost rows out of 600M is noise; 10B out of 12B is the whole problem). **Tombstoned** blocks are not just the residue of deleted rows — a delete marks **rows** deleted (making them **ghost rows**, cleared by VACUUM DELETE ONLY), whereas a tombstoned **block** is a superseded block version pending release, so **uncommitted / rolled-back writes** and in-flight transactions also hold tombstones and they can appear with no DELETE workload at all. Tombstoned blocks free at **commit**, not via VACUUM. **Never conclude "nothing to reclaim" from `stv_partitions.tossed`=0.** `tossed` counts blocks *pending release at the next commit* and is also set by a dropped column, by INSERTs, and by disk-based queries — it is a transient counter, not a measure of accumulated reclaimable space, so a table carrying billions of ghost rows can sit at `tossed`=0. Measure reclaimable space with `ghost_rows` above, and per-table tombstones with the documented query: ```sql SELECT trim(name) AS tablename, COUNT(CASE WHEN tombstone > 0 THEN 1 END) AS tombstones FROM svv_diskusage GROUP BY 1 HAVING COUNT(CASE WHEN tombstone > 0 THEN 1 END) > 0 ORDER BY 2 DESC; ``` Transient **spill**: `sys_query_detail` (`spilled_block_local_disk`, summed per query) — **temp** blocks, not growth. **"Temp blocks" covers two different consumers — separate them before prescribing anything, because the fixes do not overlap.** (1) **Rowset spill**: a query exceeding its memory grant writes intermediate results to disk; measure per query via `sys_query_detail.spilled_block_local_disk` (`query_temp_blocks_to_disk` / `blocks_to_disk` on the legacy views), and fix it with **WLM**/**QMR** limits, lower **concurrency**, or a distribution change. (2) **User temp and staging tables** from `CREATE TEMP TABLE` or an ETL scratch schema; measure them via `stv_tbl_perm` with `temp=1`, and fix by dropping them at end of ETL or letting the session disconnect. Both inflate `PercentageDiskSpaceUsed` identically, so the metric alone cannot tell you which one you have. ## Scenarios (diagnose in the order shown; note what to rule OUT) Each heading is a **signal signature, not a fixed case** — match on the *pattern* (which views disagree, which nodes diverge from the cluster average, whether the reading is transient or sustained), never on specific percentages, sizes or node counts. Any threshold quoted inside a scenario is illustrative; real clusters arrive with different numbers and the same diagnosis holds. Only error codes (**1016**, **34507**) and documented limits are literal. **Four different things put a subset of nodes ahead of the rest — do not default to slice geometry, rank them by evidence.** (a) **Slice-to-node imbalance** (S9 in `redshift-storage-remediation.md`): requires a *net* elastic resize to a node count that does not divide the slice count, AND `svv_table_info` skew ≈ 1.0. **A resize that returned to its original node count (e.g. 14→20→14) restores the original balanced layout and is NOT a slice-geometry cause — rule it out immediately, even if the customer proposes it.** **Do the slice arithmetic before offering geometry as a candidate at all.** The data-slice count is fixed at cluster creation or the last classic resize: original node count × slices per node for that type (`ra3.large`/`ra3.xlplus`/`dc2.large` = 2, `ra3.4xlarge` = 4, `ra3.16xlarge`/`dc2.8xlarge` = 16; confirm with `SELECT COUNT(*) FROM stv_slices WHERE type = 'D'`). If that count divides evenly by the *current* node count, every node holds the same number of slices and geometry cannot produce the imbalance — drop it from the answer and commit to whichever remaining cause the evidence supports, rather than presenting two candidates and leaving the customer to run the deciding query. Intermediate node counts along the way are irrelevant; only the current layout matters. (b) **Distribution-key skew** (S2 here / S13 in `redshift-storage-remediation.md`): `skew_rows` ≫ 1 on a large table concentrates rows on specific slices, so any even-ish wave of query **spill** overflows *those* nodes first while the cluster average hides it — this is the default explanation for a *transient, self-recovering* hotspot on a fixed node subset. (c) **Inbound datashare data** localized onto low-numbered slices (S6): the cluster is a datashare consumer and local permanent tables do not account for the volume. (d) **A single runaway spiller** on one node (S11): `stl_disk_full_diag` names the `query_id`. State the causal link between the hot nodes and the cause as an explicit sentence in the opening — "these nodes saturate *because* ..." — a list of numbers without that link is not a diagnosis. - **S1 — disk near-full with Disk Full 1016, VACUUM won't finish, on a table with a recurring bulk delete+reload cycle:** **start with `stl_disk_full_diag`** (above) — `temp_blocks` ≈ 0 confirms the space is **persistent data** and not **spill**, which is the first thing to establish; corroborate with `sys_query_detail` (`spilled_block_local_disk`, **temp**). Then attribute per table with `svv_table_info`/`stv_tbl_perm`/`svv_vacuum_progress`, and use `stl_vacuum` for what a vacuum actually reclaimed in **rows and blocks** — `svv_table_info`'s ghost-row figure is an *estimated row count*, not a byte measure, so judge it against `tbl_rows` rather than reading it as reclaimable bytes. Cause = **ghost row**/**tombstone** accumulation (maintenance debt, not new data). Near-full, VACUUM cannot **reclaim** — there is no free space for it to work in, and **a failed VACUUM's own undo/rollback holds space and can block the next VACUUM**, so name that chicken-and-egg explicitly rather than just retrying — so the order is strict and must be stated in this sequence: elastic **resize** / add nodes FIRST to create headroom, and only THEN `VACUUM DELETE ONLY` (never VACUUM FULL), ordered smallest-reclaimable-first, **retrying the largest table last**. Do not put VACUUM before the resize for this scenario. - **S2 — a near-full spike that self-recovers, where a contiguous subset of compute nodes saturates while the cluster average still looks comfortable:** (1) decompose with `svv_table_info` (+`stv_blocklist`/`svv_diskusage`), permanent data flat; (2) consumer is query **spill** (**temp block** / **temporary block** / **disk-based**) from a **concurrency** surge; (3) the hot nodes are **distribution key**/**distkey** **skew** onto their **slices** (cluster average masks it) — and NOT slice-to-node geometry: an elastic resize that ended back at the starting node count changed nothing about the layout, so reject that even when the customer raises it. **State the per-node causation as an explicit sentence early in the answer** — "only these nodes saturate *because* the data is distribution-skewed onto the slices on those nodes, so a roughly even spill wave overflows them first while the cluster-wide average hides it" — an answer that reports the numbers without that causal link does not count. Then give the fix list in the same breath, not at the end: **QMR** (**query monitoring rule**) on temp blocks, cap **WLM** / **concurrency scaling**, ANALYZE, fix the skew, and a per-node 85% alarm (not an average-based one). Reject permanent growth, VACUUM/tombstones, datashare reservation, a bare resize, and a single runaway query. - **S3 — querying a shared late-binding VIEW over a DATASHARE consumes far more consumer storage than the filtered result implies (error 34507 = consumer capacity):** this is expected datashare behavior, not a bug or physically-downloaded data — compare paths via `sys_query_detail`/`svl_query_summary`/`stl_scan`/`svv_table_info`/`stv_tbl_perm`/`stv_blocklist` and note that querying a shared **view** **reserves**/localizes storage for the FULL backing table regardless of WHERE (34507 = consumer capacity), because the view forces a **full refresh** re-**localization** that loses the **range-restricted** scan / **block elimination** the table path keeps (**metadata** reservation ≠ result size). Fix: share a **pre-filtered** / **persisted** / **materialized** table, a producer retention filter, **partition** the shared object, and give the consumer **headroom**. - **S5 — very high daily spill volume against low permanent utilization, with many objects in `pg_automv`:** trace via `svl_mv_refresh_status`, enumerate `svv_mv_info`/`stv_mv_info` — the cause is stale **AutoMV** (**automated materialized view**) auto-refresh, i.e. **materialized view** maintenance debt rather than data growth. Individual `pg_automv` objects can't be dropped; the only fix is setting **`auto_mv`**=false in the cluster **parameter group** and rebooting. - **S6 — a contiguous low-numbered node group materially fuller than the rest, which the customer attributes to multi-TB tombstones:** check `stv_blocklist`/`svv_diskusage`/`stv_node_storage_capacity` for capacity, then **measure the tombstone load with the two documented signals — not `stv_partitions.tossed`**: the per-table **tombstone** count from `svv_diskusage`, and `tbl_rows - estimated_visible_rows` (**ghost_rows**) from the anchor query. Both near zero ⇒ **tossed**/tombstones are tiny, nothing to reclaim — the tombstone theory is refuted and the space must be attributed to something else. **Attribute it with one measurement rather than inferring from the shape: read `svv_tables_space_usage` per node** (`local_perm_tables_space_used`, `local_temp_tables_space_used`, `datasharing_tables_space_used` against `capacity`) **and see which bucket carries the excess on the heavy nodes.** Then branch: (a) **`datasharing_tables_space_used` explains the excess ⇒ the consumer is live inbound `datashare` data localized onto those slices — commit to that as the verdict.** Shared blocks localize onto a fixed set of slices and **elastic resizes preserve that placement**, which is exactly why the imbalance survives repeated elastic resizes; a datashare **consumer** whose heavy nodes persist across resizes while local perm data doesn't account for the volume is the fingerprint. (b) **The datasharing bucket is ~0 and the imbalance is in local perm data itself, with every table at skew ≈ 1.0 ⇒ slice-to-node geometry — that is S9 in `redshift-storage-remediation.md`, not this.** Whichever nodes are heavy is incidental; the bucket is the diagnosis. Fix for (a): **classic resize** to rebuild the slice layout + producer **distribution key**/**distkey** change; not VACUUM or RMS garbage collection. - **S7 — after a resize, disk creeps up at a steady slow rate and is reset only by a periodic reboot; datasharing consumer:** decompose permanent vs datasharing per node with `svv_tables_space_usage` — `local_perm_tables_space_used` flat while `datasharing_tables_space_used` climbs is the whole diagnosis; corroborate with `svv_table_info`/`svv_diskusage`/`stv_blocklist`. Cause = **datashare** consumer **cache** **hydration** (persists at **idle**, rebuilds after **reboot**; VACUUM/ANALYZE irrelevant). Rule out tombstone + temp spill. Fix: stop the periodic **reboots**; Redshift reclaims the **cache** at internal thresholds. Not VACUUM, not scaling back nodes. **Before calling a low or dropped disk-utilization reading "expected cache behavior," check whether the drop is sudden, sustained, and lines up with a maintenance/patch date** (compare cluster version/patch history) — a drop that persists across multiple reboots after a specific patch is a possible **patch regression**, not routine cache behavior, and should be escalated with that timeline rather than waved off on timing alone. Separately, on a datashare **consumer**, a temp-block spike that tracks the **producer's** write activity (not the consumer's own workload) can be the producer's writes invalidating the consumer's cached datashare metadata, forcing a costly re-fetch for the shared tables being queried — this can dwarf ordinary query spill; the fix is coordinating producer write timing away from consumer query windows, or moving the largest shared tables to Spectrum instead of a datashare (no local metadata caching to invalidate). - **S8 — STV_TBL_PERM full of `localhost@` (temp=0, no PG_CLASS); disk sawtooths between a post-restart floor and an under-load ceiling, reset by restart:** `stv_tbl_perm`/`stv_node_storage_capacity`/`sys_query_detail`/`svv_diskusage`, plus the anchor query's `tbl_rows - estimated_visible_rows` (**ghost_rows**) and the `svv_diskusage` **tombstone** count. `localhost@` = expected RA3 cross-database internal relations (block_count = logical **managed storage** / **RMS**, not **local SSD**); not orphaned/defect. Confirm the split with `svv_tables_space_usage`: a sawtooth in `local_temp_tables_space_used` against a flat `local_perm_tables_space_used` is the signature. Sawtooth = transient **working**-set **spill** / **cache** on local SSD cleared by restart; **ghost_rows** and tombstones both ≈0 ⇒ **nothing to vacuum** (**no tombstone** to **reclaim**). Fix: reduce spill, larger node type, WLM cleanup. - **S10 — a provisioned snapshot restored into Serverless reports a footprint several times the source size; zero-ETL from Aurora in the picture:** decompose it, don't call it monolithically "expected" — attribute with `svv_table_info`, review `sys_serverless_usage`, VACUUM post-restore bloat, and prune recovery points. The three real drivers: (a) **per-slice preallocation / minimum block allocation** — Serverless spreads the restored data over many more slices and each slice takes a minimum block allocation, so the same logical data reports far larger (review `DISTSTYLE ALL` tables, which pay that cost on every slice); (b) real **zero-etl** **integration** data (`svv_integration_table_state`; pause/filter if test-only); (c) recovery-point/backup storage. - **S11 — Disk Full 1016 on a single node while managed storage still has ample free space; join keys cast to varchar:** red herring — free managed storage ≠ no disk-full. `sys_query_detail`/`svl_query_summary`/`svl_query_metrics_summary`/`stl_query_metrics`: query **spill** (`query_temp_blocks_to_disk` / `blocks_to_disk` / `spilled_block_local_disk`) exhausts the node's local SSD. The **varchar** casts defeat the **distribution** key → **broadcast**/redistribution concentrating spill on one node. Fix: remove casts, better **DISTKEY** / **DISTSTYLE**, guardrail **QMR** on `query_temp_blocks_to_disk` + `max_execution_time` + lower **concurrency**. Not VACUUM, not a capacity resize. - **S15 — `SUM(size)` from `svv_table_info` far below `SUM(used)` from `stv_node_storage_capacity`, often by more than an order of magnitude:** expected, different scopes, and an accounting question rather than an emergency — `svv_table_info` covers permanent **user tables** only, while `stv_node_storage_capacity` is raw usage including **temp**/spill, **system**/catalog, **ghost** rows, **snapshot**/**tombstone** blocks, metadata, and **datashare** blocks (RA3 capacity reflects the managed-storage quota). Break the gap down with `svv_tables_space_usage` first — it attributes the difference across perm / temp / datashared in one query — then `stv_tbl_perm` (temp=1) / `stv_blocklist` / `svv_diskusage` + `PercentageDiskSpaceUsed`; VACUUM is not the primary fix. - **S16 — any snapshot/backup billing question (bill higher than expected, "incremental so it should be cheaper", cross-region copy billed like full size), very often with AWS Backup in the picture:** **Resolve two variables BEFORE answering, because each combination has a different correct answer: the snapshot `type` (automated vs manual — and AWS Backup only ever creates *manual* snapshots, whatever the customer calls them) and the `platform` (Serverless/RG vs RA3 provisioned).** Never give one blanket "Redshift snapshots are incremental" reply, and do not answer from AWS Backup's own incremental-copy documentation even if you have it to hand — it describes AWS Backup's behavior, not Redshift's per-platform manual-snapshot metering. Pick the cell, then state the type and the platform explicitly in the answer: | Snapshot type | Serverless / **RG** | **RA3** provisioned | |---|---|---| | **Automated** | **Incremental** (only **changed** blocks after the first); free up to **100%** of **provisioned** cluster storage | Same: **incremental**, free up to **100%** of **provisioned** storage | | **Manual** (**AWS Backup** creates **manual** snapshots, so it never gets the free allowance) | Metered on **unique data blocks** across all active manual snapshots — i.e. deduplicated / incremental (since Jun 2026) | **NOT** deduplicated — each retained **manual** snapshot bills close to **its own full size** | **Lead the answer with the verdict for the matching cell, never with a general rule.** *Automated, either platform* → incremental and free up to 100% of provisioned storage, so an unexpected charge is coming from manual snapshots or retention elsewhere, not from these. *Manual on Serverless/RG* → metered on unique blocks across active manual snapshots, so the customer's "this should cost less than N full copies" intuition is **correct** and a full-size bill genuinely warrants investigation. *Manual on RA3 provisioned* → **not** deduplicated; each retained manual snapshot bills close to its own full size, so a full-size bill is **expected behavior, not a billing error**. That last cell is the one where the customer is usually right and the "it must be incremental" intuition is wrong. **Scope the two common wrong explanations to the cell — one of them is the correct answer elsewhere.** (1) "The copy is still incremental" is right for automated snapshots and for manual snapshots on Serverless/RG, and wrong *only* for manual-on-RA3 — so establish the cell before either using or rejecting it. (2) "There is no free allowance in the destination region" is never the explanation anywhere: the allowance is a **source-region, automated-only** concept offsetting automated snapshot storage against the source cluster's own provisioned capacity, it has no cross-region counterpart, and it never applied to **manual** snapshots (which is all AWS Backup creates) in either region — so its absence explains nothing about a manual-snapshot bill. Cross-region specifics, which sit on top of whichever cell applies: the **data transfer** moves only the **changed** / **incremental blocks** and is billed in the **source** region; the copy's storage in the **destination** region is billed at that region's own backup-storage (**S3**) rates, governed by the same matrix row as the source. A first/baseline copy is full by definition, so savings only appear from the second copy onward and only while a common ancestor is retained — which also means a lifecycle policy that expires the baseline forces the next copy to re-baseline at full size. Verify rather than assert: run `describe-cluster-snapshots` in the destination region and compare `ActualIncrementalBackupSizeInMegaBytes` against `TotalBackupSizeInMegaBytes` per snapshot, and confirm the matching cell's metering rule against the current Amazon Redshift backup-storage billing documentation before quoting it — the matrix reflects behavior as of Oct 2026 and metering rules change over time. ## "Is it growing, and what is driving it?" — trend and attribution are two different measurements **Scope gate — read this section only when the customer asks about growth over time or what is driving an increase** ("it keeps climbing", "what is eating the space month over month", "what changed"). **Do NOT fold it into a question about a spike, a stuck cluster, a single full node, snapshot billing, or a datashare footprint** — those are answered by the scenarios above, and adding a growth investigation to them makes the answer longer without making it more correct. When it does apply, it is the *fourth* question in the decomposition, after the first three are settled. **This question inverts the usual answer shape.** For a spike or a stuck cluster the fix is the answer and the method is supporting detail; for "what is driving the growth" the **method is the answer** — the customer is asking *how to find out* — so the opening must carry three things before any remediation: (1) the attribution method itself, stated as the ranked per-table size change over a named time window from `SYS_VACUUM_HISTORY.size_before_vacuum`, tied to each table's write pattern; (2) the attributed-vs-unattributed split, in words; (3) **what cannot be answered from customer-side views today** — composition over time is not retained anywhere public — said up front as a scoped answer, not buried as a closing caveat. Data-lifecycle remediation comes *after* those three, and name the mechanism rather than the category: a **retention limit** on the history/append tables (trim to the window the workload actually queries); **archiving cold data to S3 with `UNLOAD`** (to an encrypted bucket — add `ENCRYPTED KMS_KEY_ID ''` per SKILL.md's security defaults) and then dropping the archived rows or partitions — the only fix that removes bytes permanently, since VACUUM reclaims deleted rows but cannot shrink data the customer still keeps; a **time-series design** (date-partitioned tables behind a `UNION ALL` view) so an old partition is `DROP`ped instead of deleted and vacuumed; and **resize for headroom** when the trend is genuine growth and the data has to stay. Maintenance commands alone (VACUUM, ANALYZE) cannot fix growth — they reclaim debt, not live data — so a growth answer that stops at VACUUM has not answered it. An answer that leads with remediation and leaves the method and the limits for the end has answered a different question than the one asked. **And because a growth question is usually several questions at once, open by stating the conclusion to each question before elaborating on any of them, so the customer holds all four answers before reading any of the reasoning**: is vacuum the problem (ghost-row ratio against `tbl_rows`, so "no, N% reclaimable ≈ ~M points on the gauge, auto-vacuum already returns more than that" or "yes, table X"); what the trend is and where it comes from (CloudWatch over months); what is driving it (the write pattern, in one clause per table class); what you can and cannot attribute (the split, and that composition over time is not retained). Then, and only then, the method in full, then remediation. A reader who stops after the verdict block should hold every conclusion; the elaboration is for the reader who wants to verify them. "Storage is high" is not one question. Split it before answering: (1) is it really that full — platform first, since on RA3/Serverless local fill is storage + cache + spill; (2) is it maintenance debt — the ghost-row **ratio** against `tbl_rows`, never the raw count; (3) how much would VACUUM return — the reclaim estimate in `redshift-storage-remediation.md`; (4) **is it growing, and what is driving it** — which needs a time series that no point-in-time view can supply. This section is (4). **The cluster total comes from CloudWatch, and only from CloudWatch.** No public system view retains storage history — `STV_NODE_STORAGE_CAPACITY`, `SVV_TABLES_SPACE_USAGE` and `SVV_TABLE_INFO` are point-in-time, so yesterday's value is gone once you query today's. Pull **`PercentageDiskSpaceUsed`** with the per-node `NodeID` dimension (the cluster aggregate hides the hottest node), statistic **`Maximum`** per period (spill spikes inflate the average and self-recover), over **months, not weeks** — **a short window can invert the answer**, reading as flat when the multi-month trend is clearly up, because daily spill churn swamps a modest monthly slope. What the metric counts is **everything on disk**: permanent tables, **ghost rows** (a deleted row's block stays allocated until VACUUM rewrites it), **temp/spill**, catalog, tombstoned and datashare blocks. It cannot be decomposed, so its *slope* is the slope of the whole bucket. If the workload is stable the spill baseline is roughly stationary and the slope is mostly permanent growth — but that is an inference; say so. **Per-table growth comes from `SYS_VACUUM_HISTORY.size_before_vacuum`.** Each vacuum run logs the table's size in 1 MB blocks, so successive runs form a *measured* size series per table — the only long-horizon size source, because `SVV_TABLE_INFO` cannot be retained. Attribute the *mechanism* with `SYS_QUERY_DETAIL` (`table_id`, `step_name` = `insert` / `delete` / `merge`, and `output_rows` on the same row) and `SYS_QUERY_HISTORY.query_type` for the statement kind; repeated file loads via `STL_LOAD_COMMITS`, comparing `COUNT(*)` to `COUNT(DISTINCT filename)`. **In:out ≈ 1 does not mean "reload table"** — Redshift implements UPDATE as delete + insert, so it is equally consistent with UPDATE traffic; discriminate by checking whether the same `query_id` appears in both the insert and delete steps, and by comparing insert volume against the table's own row count to see whether it actually turns over. Retention for `STL_`/`SYS_` views is **7 days**; the S3 Tables long-term retention integration extends `SYS_*` views only. **Derived growth is not measured growth**: net rows × bytes-per-row usually lands near the measured size change but can disagree outright — a rolling window cleared by TRUNCATE reads as growing by rows while shrinking by size. Cross-check both and report a disagreement as unresolved, not as a single figure. **Three failure modes travel with this method — the third is the one that gets retracted in front of a customer.** 1. **`TRUNCATE` is invisible.** It emits no delete step, so it appears in neither `STL_DELETE` nor `SYS_QUERY_DETAIL`. A truncate-and-reload table shows all of its inserts and none of its removals and appears to grow without bound — a small staging table can compute as many times its own size. Restrict to tables above a size floor and discard any whose estimated growth exceeds a large fraction of its current size; that filter also drops legitimate fast-growing small tables, so it costs real signal and you should say which tables it removed. 2. **Per-table history covers only tables that were vacuumed.** A table auto-vacuum skipped or never selected has no rows in `SYS_VACUUM_HISTORY` and contributes nothing to the sum while contributing real bytes to the cluster. 3. **Never sum per-table growth and present it as the cluster total.** It undercounts, and always downward: unvacuumed tables are missing, TRUNCATE tables are filtered out, and per-table inference error accumulates. Expect the attributable sum to be a *fraction* of the CloudWatch total — how large a fraction varies case by case, and a substantial gap is normal, not an error. Frame it exactly as: *"the cluster grew ~X per month (CloudWatch, measured); of that we can attribute ~Y to these tables (per-table history, partial coverage); the remainder sits in unvacuumed or truncate-and-reload tables or a rising temp baseline, and we are not claiming it doesn't exist."* **If the attributable sum ever matches or exceeds the CloudWatch total, treat it as a red flag, not a success** — with three downward biases, matching means something is double-counted or a TRUNCATE table slipped the filter. And do not tell a customer their CloudWatch reading is wrong when the disagreement rests on a quota figure you inferred rather than read — reconcile, and lead with the answer to what they asked. **Composition over time — perm vs temp vs datashare as a series — is the one part of this diagnosis that cannot currently be answered from customer-side views.** `SVV_TABLES_SPACE_USAGE` gives the split for *now*; nothing public retains it. State that limit plainly rather than papering over it with the per-table sum. **Be exact about what needs escalating and what does not.** The cluster-wide *amount* of growth is fully answerable by the customer from CloudWatch — never send them to Support for a number they can read themselves. What only **AWS Support** can supply is the *composition* of that growth over time, from internal telemetry that retains the per-node permanent/temp/datashare split historically. So when the customer needs that split — "how much of the increase is permanent data versus temp versus shared" — the correct move is: give them the CloudWatch trend and the per-table attribution you *can* produce, state that composition-over-time is not exposed customer-side, and recommend a **support case** that asks specifically for the historical per-node storage composition over the period in question. A precise ask gets a precise answer; "storage is growing, please investigate" does not.