skip to content

A Redshift Spectrum query is billed per terabyte scanned from S3 — what drives that number up?

level: middleimportance: must knowfreq 65%

answer

  1. you pay for what is read, not returned
  2. row-based files cannot skip a column
  3. a predicate on the wrong column prunes nothing
  4. projection, partition pruning, format, compression
  5. a dashboard scans the same bytes every refresh

basics

~20 s

Bytes scanned depends on how much of S3 Spectrum must actually read: row-based formats force whole-file reads, unpruned partitions add prefixes, selecting unused columns adds column chunks, and weak compression inflates every byte. Columnar files, partition filters and narrow projections all cut the bill.

solid answer

~50 s

On a provisioned cluster, Spectrum is billed **on top of** the cluster, by bytes read from S3 — and it is compressed bytes on the wire, so compression cuts the bill directly. Four levers dominate. **File format**: Parquet or ORC let Spectrum read only the column chunks a query references; CSV and JSON are row-based, so every column of every touched file is read even for `SELECT one_column`. **Partition pruning**: a predicate on a registered partition column eliminates whole S3 prefixes before any file is opened; without it, every registered partition is scanned. **Projection**: `SELECT *` in a view or BI tool defeats columnar skipping entirely. **Compression**: Snappy or zstd inside Parquet shrinks the bytes actually read. The other multiplier is *frequency* — a dashboard re-scanning the same cold partitions 500 times a day pays 500 times. That is when you materialise the result locally instead.

code

sql · 10 lines
sql
-- expensive: no partition predicate, wide projection
SELECT *
FROM   spectrum.events
WHERE  event_type = 'purchase';

-- cheap: prunes prefixes, reads three column chunks
SELECT user_id, amount, event_type
FROM   spectrum.events
WHERE  event_date BETWEEN DATE '2026-08-01' AND DATE '2026-08-07'
AND    event_type = 'purchase';

go deeper

for a junior

Recall that Spectrum charges by data read from S3, and that selecting only the columns you need and filtering on the partition column are the two easy ways to read less.

for a middle

Explain the mechanics: columnar formats allow column skipping, partition predicates eliminate S3 prefixes before files open, and filtering on a non-partition column still scans everything.

for a senior

Show you would quantify it — read s3_scanned_bytes against returned bytes, spot the scan-then-discard pattern, and decide between repartitioning, format conversion and materialising locally.

for a principal

Own the cost policy: which datasets are allowed to stay external, whether scan spend is attributed back to teams, and where the hot/cold boundary sits given query volume rather than data volume.

## Where the charge comes from On a provisioned Amazon Redshift cluster, Spectrum is a **separate line item from the cluster itself**. You already pay for the nodes by the hour; Spectrum adds a charge proportional to the volume of data its scan fleet reads out of your S3 buckets for each query. Bytes are counted per query, rounded up, and a query that runs a thousand times pays a thousand times — there is no automatic caching of external scans. (Redshift Serverless bills compute in RPU-seconds rather than by the node-hour, so treat the reasoning below as the provisioned-cluster model and confirm current pricing for whichever you run.) The number to reason about is therefore always the same one: **how many bytes must Spectrum physically read from S3 to answer this query?** Every optimisation is a way of making that number smaller. ## Lever 1 — file format decides whether columns can be skipped Parquet and ORC store data column by column within row groups or stripes. When a query references three of forty columns, Spectrum reads three column chunks per row group and ignores the rest. Delimited text and JSON store data row by row: to reach one field the reader must decode the whole record, so **every column of every file touched is scanned and billed**. This is usually the single largest factor. Converting a raw CSV landing zone to Parquet routinely collapses a query's scanned bytes by an order of magnitude, and the effect grows with table width. ## Lever 2 — partition pruning eliminates whole prefixes A partitioned external table records, per partition, which S3 prefix holds its files. A predicate on the partition column lets Redshift discard non-matching partitions in the catalog, before opening a single object: ```sql -- scans one prefix SELECT count(*) FROM spectrum.events WHERE event_date = DATE '2026-08-01'; -- scans every registered partition SELECT count(*) FROM spectrum.events WHERE event_type = 'purchase'; ``` The second query filters on a data column, not a partition column, so Spectrum must read every file to evaluate it. The filter still runs — pushed down to the Spectrum layer, so few rows come back over the network — but the bytes were already scanned and already billed. **Filtering cheaply and scanning cheaply are different things.** ## Lever 3 — projection `SELECT *` reads every column chunk. This bites hardest when the wide select is hidden: a view defined as `SELECT * FROM spectrum.events`, a BI tool that materialises all fields before its own filter, or a `SELECT *` inside a CTE whose outer query needs two columns. Always project explicitly against external tables, and check what your BI layer actually sends. ## Lever 4 — compression You are billed on bytes read from S3, which are the *compressed* bytes. Snappy or zstd inside Parquet therefore reduces both the bill and the wall-clock time. Note the interaction with format: gzip-compressed CSV is small on disk but still row-based, so it scans every column — and gzip is not splittable, so it also kills parallelism. ## The multiplier nobody costs: repetition A single 4 TB scan is a one-off. The same query wired into a dashboard that refreshes every five minutes is a recurring charge that dwarfs the cluster. Two standard answers: - **Materialise it.** Redshift supports materialized views over external tables; the refresh scans S3 once and every dashboard query then reads local Redshift storage. - **Load the hot slice.** If the last 90 days serve 95% of queries, `COPY` those into a local table and leave only the deep history external. ## Diagnosing it After a query runs, `SVL_S3QUERY_SUMMARY` reports what the Spectrum layer actually did: `s3_scanned_bytes`, `s3_scanned_rows`, the rows and bytes returned to the cluster, and how many files and splits were involved. `SVL_S3PARTITION` reports `total_partitions` versus `qualified_partitions` — the direct measure of whether pruning worked. If scanned bytes are large but returned bytes are tiny, you are paying to read data you immediately throw away, and the fix is layout (partitioning, format), not the query's filter. ## The month-over-month surprise When the same query scans ten times more than it did last month, the causes are almost always: more partitions accumulated and the query has no partition predicate; someone widened the table so `SELECT *` now costs more; an upstream job started writing CSV instead of Parquet; or a Glue crawler registered a second copy of the data under the same table.

  • A Spectrum query reports huge s3_scanned_bytes but tiny s3query_returned_bytes. What does that tell you?
    The filter is working but the layout is not. Spectrum read a large volume from S3, applied the predicate in its own layer, and returned almost nothing — so you paid for bytes you discarded. The fix is physical: partition on the filtered column, or convert the files to a columnar format so the unused columns are never read.
  • Does compressing CSV files with gzip reduce Spectrum's bytes scanned?
    It reduces the bytes read from S3, so it does reduce the scan charge. But it does not restore column skipping — CSV is still row-based, so every column is decoded. And gzip is not splittable, so one file is processed by a single reader. Parquet with internal compression gives you both savings.
  • How would you stop a five-minute dashboard refresh from re-scanning the same S3 partitions forever?
    Materialise the result. A materialized view over the external table scans S3 on refresh and serves every dashboard hit from local Redshift storage, so the scan cost becomes per-refresh rather than per-viewer. If the underlying slice is small and hot, loading it into a local table with a sort key is even better.

saying these in an interview costs you the question

  • Thinking the cluster hourly cost already covers Spectrum scans
  • Believing any WHERE clause reduces bytes scanned
  • Assuming gzipped CSV is as cheap to scan as Parquet
  • Expecting Redshift to cache external scans automatically
  • Confusing rows returned with bytes scanned when estimating cost

context