Skip to content

perf(r2rml): push numeric (double/decimal) FILTER predicates to Iceberg pruning (PR-7, H4) - #1494

Closed
aaj3f wants to merge 2 commits into
perf/r2rml-pr4c-optional-starfrom
perf/r2rml-pr7-numeric-stats
Closed

perf(r2rml): push numeric (double/decimal) FILTER predicates to Iceberg pruning (PR-7, H4)#1494
aaj3f wants to merge 2 commits into
perf/r2rml-pr4c-optional-starfrom
perf/r2rml-pr7-numeric-stats

Conversation

@aaj3f

@aaj3f aaj3f commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

PR-7 (H4): push numeric (double / decimal) FILTER predicates down to Iceberg file / row-group pruning, so a numeric range/equality filter skips data files instead of decoding all of them. Previously only int / date / string predicates pruned; a decimal or double FILTER decoded every file.

The two-layer blind spot

  • Query side (primary): ScanValue carried no numeric type, so to_scan_value never produced a numeric predicate, build_iceberg_filter never emitted one, and the reader never saw it — the real reason q019 showed files_pruned = 0.
  • Iceberg side: stat_bounds had no Double / FLBA-decimal arm, prunable_stats declined all decimals, and TypedValue::lt/le had no Decimal arm (a pushed decimal would silently never prune).

Both are closed here, gated by FLUREE_ICEBERG_NUMERIC_STATS (default on).

Change surface

Query: ScanValue +Double/Decimal; to_scan_value gated arms + scale-normalizing BigDecimal decompose; build_iceberg_filter Double→Float64 (double cols), Decimal→Decimal (decimal cols). An integer literal against a decimal column (q019 ?deb > 1000000 on the xsd:decimal money column) coerces to an EXACT scale-0 decimal via int_pushdown_literaldecimal_cmp normalizes the scale gap. Gated api-side (FLUREE_ICEBERG_NUMERIC_STATS OnceLock, disk-catalog-cache precedent); switch-off keeps the pre-PR-7 Int64 no-prune form (full revert).

Iceberg: stat_bounds +Double/Float/FLBA-decimal (decodes the column's scale, not the literal's); prunable_stats admits FLBA-encoded decimals (the encoding the Iceberg spec mandates), declines INT32/INT64-backed decimals with a debug breadcrumb; TypedValue::lt/le +Decimal arm + NaN-safe Float.

Correctness

The pushdown is a strict superset — it may over-keep, never over-prune; the in-engine FILTER is the sole authority. A latent NaN over-prune (F15; unreachable pre-PR-7, armed by the Double push) is fixed in-PR: a NaN operand in Float lt/le returns None → keep. Two conservative residuals decline observably (double-vs-decimal, decimal-vs-int; inexact / no exact cross-type compare → keep + debug breadcrumb).

H4 live gate (DW_SF01, ~7670 files per fact table)

query files_pruned / selected wall (hot) note
q019 7670 / 0 800ms integer > money threshold; cold 38.8s → 4.0s (~10×)
q020 7579 / 91 635ms date range — representative partial prune
q021 7670 / 0 636ms int account-code band
q019 switch-off 0 / 7670 916ms byte-identical → perfect revert

Native 54/54 parity: 0 hash mismatch. Snowflake decimals ARE FLBA in practice (the prune worked end to end).

Caveat: q019/q021 select zero rows on this dataset, so their total prune is the predicate-selects-nothing case (the strongest but easiest prune); a partially-selective filter prunes proportional to data layout (q020's 91-file partial is the representative middle). This is a different case from the 05-diagnosis "int pushes but nothing prunes" note, which measured an in-range predicate over the column's value spread — q021 here is an out-of-range band — so it is not a regression of that finding.

Tests

11 new hermetic unit tests over real Parquet fixtures: double prune/keep, FLBA-decimal cross-scale (9.99↔9.990) prune/keep/boundary, int→scale-0-decimal coercion (positive prune / in-bounds keep / rescale-overflow keep / switch-off revert), INT-decimal decline, NaN-keep, and the BigDecimal decompose. iceberg lib 172, query r2rml 59, api r2rml 14 — all green; clippy + fmt clean.

Stacked on perf/r2rml-pr4c-optional-star. PR-5 (scan-side top-k), whose pruning leg these numeric stats unblock, is the next and final roadmap item.

…rg pruning (PR-7, H4)

Widen file/row-group pruning to double and decimal so numeric FILTERs skip data
files, not just int/date/string. The blind spot was two-layer: the query side
never produced a numeric ScanValue (so no numeric predicate reached the reader --
the real cause of q019 files_pruned=0), and the iceberg side would have declined
it anyway (no Double/FLBA-decimal stat_bounds arm; no Decimal arm in
TypedValue::lt/le). Both are closed here, gated by FLUREE_ICEBERG_NUMERIC_STATS.

Query side: ScanValue gains Double + Decimal; to_scan_value pushes them (gated)
with a scale-normalizing BigDecimal decompose. build_iceberg_filter bridges
Double->Float64 (double cols) and Decimal->Decimal (decimal cols). An INTEGER
literal against a decimal column (q019's `?deb > 1000000` on the xsd:decimal money
column) coerces to an EXACT scale-0 decimal via int_pushdown_literal so it is
comparable to the column's decimal bounds (decimal_cmp normalizes the scale gap)
-- gated by an api-side OnceLock mirroring the query switch, so switch-off keeps
the pre-PR-7 Int64 (no-prune) form and the revert guarantee holds.

Iceberg side: stat_bounds gains Double/Float and FLBA-decimal arms (decoding the
column's scale, not the literal's); prunable_stats admits FLBA-encoded decimals
(the encoding the Iceberg spec mandates) and declines INT32/INT64-backed decimals
with a debug breadcrumb; TypedValue::lt/le gain a Decimal arm and NaN-safe Float
arms.

Correctness: the pushdown stays a strict SUPERSET (over-keep, never over-prune;
the in-engine FILTER is the sole authority). NaN over-prune (F15) -- latent and
unreachable pre-PR-7, armed by the Double push -- is fixed in-PR: a NaN operand in
Float lt/le returns None -> keep. Two conservative residuals are declined
observably (double literal vs decimal column, decimal literal vs int column;
inexact / no exact cross-type compare -> keep + debug breadcrumb).

H4 live gate (DW_SF01, ~7670 files per fact table):
  query          files_pruned/selected  wall (hot)  note
  q019           7670/0                 800ms       integer > money threshold; cold 38.8s -> 4.0s (~10x)
  q020           7579/91                635ms       date range -- representative partial prune
  q021           7670/0                 636ms       int account-code band
  q019 switch-off 0/7670                916ms       byte-identical -> perfect revert
Native 54/54 parity: 0 hash mismatch. Snowflake decimals ARE FLBA in practice
(the prune worked end to end).

Note on the total prunes: q019/q021 select zero rows on this dataset, so their
full prune is the predicate-selects-nothing case (the strongest but easiest
prune); a partially-selective money filter prunes proportional to data layout,
with q020's 91-file partial the representative middle. This is a different case
from the 05-diagnosis "int pushes but nothing prunes" observation, which measured
an in-range predicate over the column's value spread -- q021 here is an
out-of-range band -- so it is not a regression of that finding.
@aaj3f
aaj3f force-pushed the perf/r2rml-pr4c-optional-star branch from b1cb988 to edcd07b Compare July 15, 2026 16:48
…case

scan_value_from_bigdecimal on a round literal produces a NEGATIVE scale via
normalized() (unscaled 1, scale -6 = 1_000_000). decimal_cmp handles it by
construction (max-scale rescale keeps exponents >= 0) but no test pinned it. Add
a negative-scale-vs-positive-scale round trip to decimal_lt_le_cross_scale (lt/le/
ge, exact-equality across the sign boundary) — the last untested arm (#1494).
@aaj3f

aaj3f commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Consolidating at maintainer request. #1494 (PR-7 — numeric double/decimal FILTER pushdown to Iceberg pruning) is carried forward in #1507 (the R2RML/Iceberg virtual-dataset performance program), which brings the #1475#1507 lineage forward as a single reviewable unit. Its verification of record is the program's corpus benchmark, published as C2-bench-wave1.md (native == virtual, 0 hash mismatches). The pre-consolidation branch tip is preserved at tag archive/pre-refactor-2026-07-21/perf_r2rml-pr7-numeric-stats — no history was rewritten. Closing in favor of #1507; discussion continues there.

@aaj3f aaj3f closed this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant