Lakshya

Databricks & Snowflake · Chapter 12 of 16

Query execution: Photon, micro-partitions and the profile

What the engines actually do, and the three questions that diagnose any slow query on either platform.

6 min read0 diagramsAll 16 chapters

Both vendors publish enough detail to reason about properly, and both publish performance numbers you should not repeat. This chapter separates the mechanisms — which transfer — from the multipliers, which do not.

Photon: a vectorised C++ engine inside the JVM

Photon is a set of physical operators written in C++, running in the same process as the Java Virtual Machine (JVM) engine. Two design decisions are documented and both are more interesting than the benchmark numbers.

  • Vectorised interpretation rather than code generation. The authors concede code generation is better for some cases — complex conditional expressions in particular — and chose vectorisation anyway, for two stated reasons. It enables runtime adaptivity: the engine discovers characteristics of each micro-batch and takes specialised code paths accordingly. And it was easier to build, profile, debug and operate, because preserving operator boundaries lets you collect rich per-operator metrics.
  • C++ rather than the JVM, for three stated reasons: hitting a performance ceiling; JIT bailouts creating performance cliffs, where exceeding a method-size limit silently deoptimises a hot path; and native performance being easier to explain, with explicit control over memory and vector instructions.

The adaptivity is the part worth teaching. Every kernel specialises on at least two booleans — does this batch contain nulls? and does it contain inactive rows? — as compile-time template parameters, so the branch disappears entirely. Further specialisations include mostly-ASCII strings versus general Unicode, and compacting a sparse batch before a large hash probe, because dense batches parallelise memory loads while sparse ones stall on latency without saturating bandwidth.

The hash join is three vectorised steps — hash, probe to load entry pointers, then column-by-column key comparison producing a list of non-matches — with quadratic probing. The stated source of the speedup is masking cache-miss latency by parallelising loads, and they deliberately chose that over manual software prefetching because prefetch tuning is hardware-specific and does not transfer.

What Photon does not do, which matters more operationally than what it does.

It does not accelerate user-defined functions, the RDD or Dataset APIs, or stateful streaming. It is not worth enabling for queries that already finish in under two seconds.

When it meets an unsupported operator it falls back to the JVM engine transparently, with correct results — and this is the expensive failure mode, because you keep paying the higher Photon rate while running on the slower path. A single Python UDF in the middle of a plan can do this.

The integration rule explains why: a planner walks the plan bottom-up from the file scans and stops at the first unsupported node, deliberately never starting mid-plan, to avoid regressions from repeated columnar-to-row pivots. So an unsupported operator low in the plan disables Photon for everything above it.

The practical check: look at the query profile for how much of the plan is actually on the accelerated path. If a UDF sits near the leaves, removing or rewriting it can change the whole plan's engine.

Snowflake: micro-partitions and clustering depth

Snowflake stores data in micro-partitions of 50 to 500 MB uncompressed, created automatically in insertion order. Each carries metadata: the value range per column, the number of distinct values per column, and other optimisation properties. Pruning happens in two stages — eliminate micro-partitions on metadata, then eliminate columns within the survivors.

The insertion-order detail is the one that matters. Your physical layout is a consequence of the order you loaded data in, which is why a table loaded by daily batch naturally prunes well on date and badly on everything else, and why clustering exists.

  • Clustering depth is the average overlap depth across micro-partitions for the given columns. Lower is better; the minimum is 1.
  • The documentation is unusually candid: query performance is the best indicator of how well-clustered a table is — depth is a proxy, not the target.
  • Guidance worth memorising: clustering is worth it when the table has a large number of micro-partitions, typically multiple terabytes; use a maximum of three or four columns; and order them from lowest cardinality to highest.
  • Very high cardinality columns are poor keys directly — a nanosecond timestamp is the documented example. Reduce the cardinality with an expression instead, such as truncating a timestamp to a date.

Adaptive query execution, and its documented limits

Adaptive execution re-plans at the end of a shuffle or broadcast exchange, using actual statistics rather than estimates. Four capabilities: switching a sort-merge join to a broadcast join, coalescing shuffle partitions, splitting skewed partitions, and detecting empty relations.

The limits are what you need in an interview:

  • It does not reorder joins. Join order is decided before execution and stays decided.
  • It cannot broadcast the left side of a left outer join, or either side of a full outer join.
  • Skew detection requires a partition to be both larger than a size threshold and more than five times the median. Both conditions. Which is exactly why moderate skew on a modestly-sized shuffle is silently not handled, and why you can have a visibly skewed job that adaptive execution ignores.

Reading a query profile — the same three questions on both platforms

The vocabulary differs and the diagnosis does not. Every slow query is one of three things:

QuestionSnowflakeDatabricks
Did I read too much?Partitions scanned against Partitions total. If they are close, pruning failedThe percentage of data pruned shown on the scan node
Did I materialise too much?An exploding join — output rows far exceeding input rows, usually a missing join condition. Also UNION without ALL, which adds a needless dedupRows out versus rows in on the join node; the Top operators panel
Did I run out of memory?Bytes spilled to local storage and to remote storagePeak memory per operator and spill metrics

Two platform-specific gotchas worth carrying. On Snowflake, a small nonzero remote spill can be an artefact of the query acceleration service rather than a problem, so do not chase it. And the Most Expensive Nodes panel lists every node taking at least one percent of execution time, which is where to start rather than at the top of the DAG.

On the benchmark wars — say this out loud

Every headline performance figure in this space is vendor-run. The Photon multipliers come from Databricks' own paper. The record-setting benchmark exchanges of the early 2020s were a marketing fight in which each side configured its own runs. Vendor-adjacent comparisons for every other engine have the same problem.

The mechanisms transfer and are checkable. A vectorised hash table really does mask cache-miss latency. Micro-partition pruning really does depend on insertion order. Adaptive execution really does have a two-condition skew threshold.

The multipliers do not transfer. The right question about any performance claim is: what was the query shape, who configured the run, and does the mechanism apply to my workload? Teaching that question is worth more than any number, and asking it in an interview is a strong signal.

Check yourself

Why did Photon choose vectorisation over code generation?Runtime adaptivity — specialising per micro-batch on nulls and sparsity — plus operability: preserved operator boundaries make profiling and metrics possible.
What is the expensive failure mode of Photon?Silent fallback. An unsupported operator such as a UDF puts the plan on the JVM path while you keep paying the higher DBU rate.
Why does Snowflake clustering exist?Because micro-partitions are created in insertion order, so the physical layout reflects load order rather than query patterns.
Why does adaptive execution sometimes ignore obvious skew?It requires a partition to be both above an absolute size threshold and more than five times the median. Both conditions must hold.
← Table format internals: the log, the tree, and the timelinePerformance engineering with the actual numbers →