skip to content

Why do filters on a Snowflake VARIANT path sometimes prune as well as a typed column and sometimes not?

level: seniorimportance: nice to knowfreq 36%

answer

  1. It is not stored as text to re-parse
  2. Regular shapes get treated like real columns
  3. Type consistency along a path is the hinge
  4. Hidden sub-columns carry their own min/max
  5. Irregular paths force reading the whole document

basics

~20 s

During ingest Snowflake extracts consistently typed VARIANT paths into hidden sub-columns that carry their own min/max metadata, so filters on them prune like typed columns. Irregular or mixed-type paths are not extracted and force a full document read.

solid answer

~50 s

Snowflake does not store a `VARIANT` as opaque text. As data lands it extracts as much of the document as it can into an internal **columnar** representation: paths that are present consistently and hold a consistent type are materialised as hidden sub-columns with their own min/max metadata in the micro-partition header. A predicate on such a path prunes micro-partitions and reads only that sub-column, so it performs close to a native column. Paths that are irregular — present in a minority of documents, holding a string in some rows and an object in others, or buried in highly variable structures — are much less likely to be extracted; querying them means reading and parsing the whole `VARIANT` for every candidate row. Snowflake does not publish the exact rule, so the reliable answer for hot fields is to project them into typed columns and keep the raw `VARIANT` alongside as the replayable archive.

code

sql · 8 lines
sql
-- filter straight on the VARIANT path
SELECT COUNT(*) FROM orders_raw
WHERE payload:status::string = 'PAID';

-- same predicate on a projected typed column
SELECT COUNT(*) FROM orders
WHERE status = 'PAID';
-- compare 'partitions scanned / partitions total' in Query Profile

go deeper

for a junior

Know that Snowflake parses semi-structured data on load rather than on every query, so a VARIANT column is not just stored JSON text.

for a middle

Explain that consistently typed, regularly present paths get extracted into internal sub-columns with their own metadata, and name the shapes that defeat that.

for a senior

Diagnose with partitions-scanned evidence rather than folklore, and design the hybrid: raw VARIANT kept for replay, hot fields projected into typed columns that prune predictably.

for a principal

Own the schema-drift bargain across the platform — where flexibility is worth unpredictable scans, what contract producers owe on field types, and how much duplicated storage the query savings justify.

## The thing most candidates get wrong The intuition is that `VARIANT` is a JSON blob and every query re-parses it. That is not how Snowflake stores it. At load time the document is parsed once, and Snowflake extracts as much of it as it can into an internal columnar form — effectively hidden sub-columns, each with the same per-micro-partition statistics (min, max, distinct-ish counts, null counts) that ordinary columns get. That is why `WHERE payload:status::string = 'PAID'` can prune micro-partitions and read a narrow slice of storage, while looking, from the SQL, exactly like an expensive document scan. ## When extraction works well Extraction favours regularity. Paths that behave like columns get treated like columns: - the path appears in most or all documents; - its value has a **consistent type** across rows — always a string, always a number; - it is a scalar at a stable position in the structure, not a value whose location shifts between documents. A payload that is really a flat record with ten well-known fields, delivered as JSON because that is what the producer emits, behaves close to a normal table. ## When it degrades Extraction cannot help when the shape fights it: - **Mixed types on one path.** `payload:id` numeric in some rows and a string in others. Type inconsistency is the classic blocker. - **Sparse or exploding key spaces.** Documents that use values as keys — `{"metrics": {"cpu_9f3a": 0.4}}` — generate an unbounded set of paths that cannot each become a sub-column. - **Values buried inside arrays.** Elements are reached positionally at query time via `FLATTEN`, and per-element values are far less amenable to the same column-style treatment than stable top-level scalars. - **Very large documents.** With a 16 MB per-value ceiling, big documents mean fewer rows per micro-partition and more bytes touched per qualifying row. When a path is not extracted, evaluating a predicate on it means reading the stored document and walking it — no pruning benefit, and CPU proportional to document size times rows scanned. ## Diagnosing it You cannot list the sub-columns; there is no catalogue view for them. You diagnose empirically: - Run the predicate against the `VARIANT` path and look at the Query Profile's **partitions scanned versus partitions total** on the table scan. A ratio close to 1 on a selective predicate means no pruning happened. - Compare against the same predicate on a typed column materialised from the same path — the difference in bytes scanned is the cost of non-extraction. - Watch for a scan whose time is dominated by processing rather than remote I/O; that shape suggests per-row document parsing. Be careful not to blame extraction for a self-inflicted wound. Wrapping the path in a function (`LOWER(payload:status::string) = 'paid'`) defeats pruning regardless of how well the path is stored, exactly as it would on a native column. ## The production pattern Keep the raw document and project the hot fields: ```sql CREATE TABLE orders ( order_id NUMBER, status STRING, order_ts TIMESTAMP_NTZ, payload VARIANT -- the original, kept for replay ); INSERT INTO orders SELECT payload:order_id::number, payload:status::string, payload:created_at::timestamp_ntz, payload FROM orders_raw; ``` This buys three things: predictable pruning on the typed columns, the option to define a clustering key on one of them, and a stable contract for analysts who should not have to know the payload's shape. It costs some storage — the field is stored twice — which compression makes cheap relative to the scan savings on a hot dashboard. The alternative extremes are both worse in practice. Storing only the `VARIANT` leaves query performance at the mercy of a rule you cannot inspect. Storing only the flattened columns throws away the original, so a field nobody projected is gone and any schema-drift correction requires a re-ingest from the source files. ## The trade-off to voice Semi-structured storage buys tolerance for schema drift: a producer adding a field breaks nothing, and the new field is queryable immediately. What it costs is predictability — you cannot see whether a given path is extracted, and the answer can change as the data's shape drifts under you. So take the flexibility at the landing zone, and pay for determinism on the small set of fields that dashboards actually filter on. ## What interviewers are checking That you know `VARIANT` is stored columnar rather than as text; that you can name the shapes that defeat the optimisation; that you diagnose with partitions-scanned rather than folklore; and that your recommendation is the pragmatic hybrid rather than a dogmatic "never use VARIANT" or "VARIANT is free".

  • How would you confirm empirically that a VARIANT predicate is not pruning?
    Run it and read the Query Profile's table-scan node: compare partitions scanned against partitions total. A selective predicate that still scans nearly every micro-partition is not pruning. Cross-check by materialising the same path into a typed column and re-running — the drop in bytes scanned quantifies what extraction was not giving you.
  • Does wrapping a VARIANT path in a function affect pruning?
    Yes, and the same way it would on an ordinary column: `LOWER(payload:status::string) = 'paid'` makes the predicate non-sargable against the stored min/max metadata, so pruning is lost regardless of how well the path is stored. Normalise at write time or compare against the stored form instead of transforming the column in the filter.
  • Why keep the raw VARIANT once the hot fields are projected into typed columns?
    Because projection is a decision made with today's questions. Keeping the original document lets you add a field later without re-ingesting from stage, gives you an audit trail of exactly what the producer sent, and absorbs schema drift without a pipeline change. Compression keeps the duplication cheap relative to the scan savings on typed columns.

It is like a filing clerk who, as post arrives, pulls out the fields that every letter has — date, sender, amount — into index cards. Letters that put those in different places, or in different formats, can only be answered by opening the envelope.

saying these in an interview costs you the question

  • Believing VARIANT is stored as raw JSON text and parsed per query
  • Assuming any VARIANT path always prunes like a typed column
  • Ignoring type inconsistency along a path as the cause
  • Using object keys as data values in high-cardinality payloads
  • Refusing VARIANT entirely instead of projecting the hot fields

context