Why architecture matters here

Row-at-a-time execution is a mismatch with silicon at every level, and it pays for the mismatch per row — billions of times per query. Interpretation overhead: each row visits an operator tree through virtual dispatch; the bookkeeping around processing a value costs more than processing it. Memory behavior: rows-as-Object[] scatter fields across the heap; every access is a pointer chase, every pointer chase a potential cache miss, and the CPU — capable of dozens of operations per nanosecond — spends most of its time waiting on DRAM. Branching: per-row null checks and type dispatch defeat the branch predictor. Vectorization attacks all three at once: amortize dispatch over 1,024 rows (the tree is walked per batch), lay values out as contiguous primitive arrays (the prefetcher's favorite shape, and the JIT auto-vectorizes the loops into SIMD), and hoist null/type decisions out of the loop (a batch knows if a column has no nulls — the no-null loop has no branch at all).

Why does this matter operationally and not just academically? Because warehouse economics are CPU economics. Scan-filter-aggregate is the shape of most BI and ETL work; on columnar storage the IO is already efficient, so CPU is the bottleneck — and a 5× CPU reduction is 5× more queries per cluster, or the same queries on a fifth of the hardware. It also compounds with everything else: vectorization is why LLAP's cache serves sub-second queries (cached decoded vectors + tight loops), why predicate pushdown's surviving rows are cheap to process, and why the CBO's chosen plan actually runs at the speed the cost model imagined.

And the failure mode is uniquely sneaky: fallback is silent and total per operator-path. One non-vectorized UDF in a projection, one exotic type in a key column, and the affected operators run row-mode — correct results, 5× the CPU, no error anywhere. The 'why is this query slow' investigation that ends at notVectorizedReason in the explain output is a rite of passage; making that check routine is the difference between a warehouse that stays fast and one that decays one convenient UDF at a time.

Advertisement

The architecture: every piece explained

Top row: the data structures. A VectorizedRowBatch holds up to 1,024 rows as an array of ColumnVectors: LongColumnVector (all integer types, dates, booleans as longs), DoubleColumnVector, BytesColumnVector (byte arrays + offsets/lengths — strings without per-value objects), DecimalColumnVector, TimestampColumnVector, and nested types via List/Map/StructColumnVector. Each vector carries a noNulls flag and an isNull bitmap, plus an isRepeating flag that collapses constant-valued columns to a single slot (partition keys cost one value per batch, not 1,024). The batch's selection vector (selected[] + selectedInUse) lists the live row indices: a filter marks survivors by writing indices, and every downstream loop iterates selected — filtering without materializing.

Middle row: the compute. Expression templates are the trick that keeps loops monomorphic: rather than one generic evaluator, Hive ships (mostly code-generated) classes per operation×type combination — LongColGreaterLongScalar fills a selection vector from a comparison; DoubleColMultiplyDoubleColumn writes an output vector; LIKE, IN, BETWEEN, CASE, casts, and date functions all have vector forms — each a branch-minimal loop over primitive arrays with separate no-null/nullable paths. Vectorized joins and aggregations extend the model: map-join probes hash tables with batched key columns (optimized fast paths for single long/string keys), group-by aggregates maintain per-group states keyed by batched hashes, and vectorized reduce-sink serializes batches for shuffle. Row-mode fallback is the boundary: legacy UDFs (non-vectorized GenericUDFs get wrapped in the adaptor — better than full fallback but still per-row), unsupported expression/type combinations, or certain operator configs mark the plan (or a vertex) not-vectorized, visible in EXPLAIN VECTORIZATION output with reasons.

Bottom rows: the ecosystem fit. The ORC reader decodes stripes straight into ColumnVectors — RLE runs become isRepeating spans, dictionary-encoded strings fill BytesColumnVectors efficiently — and applies row-group-level predicate skipping before any batch is built; Parquet's vectorized reader does the analogous work. LLAP synergy: the daemon cache stores decoded column chunks, so a hot query's batches are assembled from cache memory with zero decompression — vectorization is what makes those cache hits translate into sub-second answers. The CPU effects row names the physics: sequential array access (prefetch-friendly), JIT auto-SIMD on the tight loops, and batch-hoisted branches. The ops strip: EXPLAIN VECTORIZATION DETAIL as the diagnostic, fallback hunting as routine, and UDF policy (vectorized implementations or the adaptor, never silent row mode) as governance.

Hive vectorized execution — batches of 1024, not rows of 1the CPU-efficiency layer under every fast queryVectorizedRowBatch1024 rows, columnarColumnVectortyped arrays + null mapVectorized opstight loops per expressionSelection vectorfilters without copyingORC readercolumn stripes → vectorsExpression templatesgenerated per type comboVectorized joins/aggshash tables on batchesFallback to row modeunsupported opsCPU effectscache locality, SIMD, branch predictLLAP synergycached vectors, no re-decodeOps — explain vectorization + fallback hunting + UDF adaptationfillevaluateselectdegradefeedspeedreuseoperateoperate
Hive vectorization: ORC stripes fill columnar 1024-row batches; generated tight loops evaluate expressions; selection vectors filter without copying.
Advertisement

End-to-end flow

Follow one query through the vector machinery: revenue by day for a month, filtered to one store, over a 2B-row ORC fact table. The scan operator asks the ORC reader for the month's stripes; row-group stats (min/max on store_id) skip 92% of row groups before any decode. Surviving row groups decode three columns — store_id, sale_date, amount — directly into a batch: store_id as a LongColumnVector (dictionary-decoded), amount as DecimalColumnVector, sale_date as LongColumnVector of day numbers. The filter FilterLongColEqualLongScalar sweeps store_id's array and writes 41 surviving indices into the selection vector — no copies. The group-by operator hashes sale_date for the 41 selected rows (a batched hash over a long column — the fast path), accumulates decimal sums per group, and every thousand batches emits partial aggregates. Per-value work in the hot loop: an array load, a compare, occasionally a hash-and-add. The query touches 170M post-skip rows and completes in 6 seconds of CPU across the cluster; the row-mode equivalent, benchmarked once for the capacity plan, took 41.

Now the regression story every warehouse eventually lives. An analyst wraps amounts in a convenience UDF — normalize_currency(amount, currency) — implemented years ago as a GenericUDF. The nightly dashboard slows from 6 seconds to 29. Nothing errored; the profile just shows the scan vertex's CPU quintupled. EXPLAIN VECTORIZATION DETAIL tells the story in one line: the projection is vectorized via the VectorUDFAdaptor — every batch unpacked to rows for the UDF, repacked after — and a second expression in the same projection fell back entirely with notVectorizedReason: UDF not supported. The fix has three tiers, and the team documents them as policy: best, implement the UDF as a VectorizedExpression (a day's work, restores 6s); good, ensure the adaptor path (acceptable overhead for rarely-hot UDFs); unacceptable, silent row mode on tier-1 dashboards. They also add the check to CI: new queries against tier-1 tables run EXPLAIN VECTORIZATION in review, and 'notVectorizedReason' in the output blocks the merge with the same severity as a failing test — because it is one: a 5× performance regression, caught by reading the plan instead of the pager.