Research cross system query dissection - kmosoti/FabricO11y GitHub Wiki

Historical report imported 2026-10-10 from docs/research/cross-system-query-dissection.md. Original SHA-256: 4ba4595200df35e1135aa43e27d82c185ee9096ffa15e5a939829985eba5e258. Measurements and outcomes retain their stated revisions. Links and local environment labels are normalized; $FABRIC_DATA is the mounted experiment drive. See evidence policy.

Query and index source dissection

Status: selected implementation inspection, 2026-10-08; no upstream code or comparative workload executed. Tier 6 research, subordinate to the product contract, query contract and accepted ADRs. This extends the cross-system program with concrete sources; it does not adopt an engine, index or format.

Evidence boundary and problem model

The retained downloads live under cross-system source evidence (local evidence omitted from stage). Each successful source directory has a receipt.json, a complete regular-member inventory and a selected-source.tar.gz with selected member SHA-256 values in the receipt. The observations below come from reading selected archive members in memory, without extracting or executing upstream files. Download/readback verification is distinct from implementation correctness or performance. The supplementary receipt (local evidence omitted from stage) records bounded pinned-file retrievals with per-file command, exit, size and SHA-256. Its selected archive prefixes each member with its source identity. These supplements extend inspection at the same pins; the failed initial ClickHouse archive download remains failed.

The useful question is how much verified work a candidate avoids over its actual lifetime, including construction, publication, candidate decoding, exact predicate verification, payload materialization and reclamation. An index can save row decoding while increasing integrity reads, request startup or maintenance costs. An optional filter is an overapproximation; Fabric still requires exact literal substring semantics, stable ordering, full paginated answers and the current snapshot/retention behavior. Raw custody and typed validation remain mandatory.

Dimensions that separate credible trajectories are predicate type, match density, clustering, payload width, query reuse before expiry, candidate representation, physical order and ownership lifetime. A dense negative workload is required even when a sparse positive case is attractive. These are different mechanisms and conditions, not seven engines competing on an unregistered synthetic score.

Implementation observations

Tantivy: three representations are three costs

Pin c12cc2d54bc0ff75bf1b4a836b4c7e593600bc1a; receipt (local evidence omitted from stage). In src/indexer/segment_writer.rs, add_document feeds the inverted index, fast fields and document store separately. Serialization writes those three representations, then custom plugins; mem_usage sums their estimates. Fast fields do not eliminate the postings or stored payload cost when all are enabled.

src/postings/block_segment_postings.rs decodes document IDs in compressed blocks. Requested record options control whether frequency blocks are decoded. seek first seeks a block, loads it, then seeks within the decoded block; reset reuses the cursor for another term without reallocating that cursor, but explicitly does not reset the positions list. A postings hit is an address candidate, not the original Fabric log or a complete answer envelope.

Deduction: an unscored Fabric candidate path should avoid frequency/position work unless its actual semantics require them. This does not establish that Tantivy's ordinary token semantics implement arbitrary case-sensitive UTF-8 substrings. Supplementary src/tokenizer/simple_tokenizer.rs splits on non-alphanumeric Unicode characters. ngram_tokenizer.rs uses CodepointFrontiers to enumerate codepoint-boundary slices of configured length; prefix-only mode stops after the leading edge. This is neither Fabric's existing byte-trigram filter nor an exact substring decision. A sound adaptation needs an explicit byte/codepoint mapping, non-prefix mode where required, short-needle fallback and final exact substring verification. No equivalence test ran.

Quickwit: merge and cache bounds have narrower scopes

Pin d140cfc4bbcc37b73354a9d0e4f4b1a44639bb04; receipt (local evidence omitted from stage). quickwit/quickwit-indexing/src/actors/merge_executor.rs constructs a stacked union directory with an output directory, shadow metadata and source directories. The output uses sequential mmap advice and controlled I/O carrying the actor's kill switch and progress state. It opens a Tantivy writer with one thread and a 15,000,000-byte memory budget, applies delete queries when present, commits them, then merges segment IDs. A single segment with no deletion skips merging.

quickwit/quickwit-indexing/src/split_store/split_store_quota.rs checks both cached split count and cached bytes before accepting another split. Those counters describe cache residency, not whole-process memory, mmap residency or the active merge's input/output overlap. Supplementary quickwit/quickwit-indexing/src/actors/publisher.rs acquires a publish lock and sends staged IDs, replaced IDs, a checkpoint delta and a publish token together in a metastore request. It suggests source truncation only after that request succeeds. Invalid tokens terminate the pipeline; a dead publish lock prevents another publication. This caller couples publication ownership and input progress explicitly. The metastore transaction was not selected, so its atomicity is not established by this file alone.

Deduction: avoiding redundant consolidation can dominate tuning a merger. A Fabric index prototype must count rebuild/merge work and coexisting old/new files. The observed actor controls and budget do not prove a global bound or give permission to change Fabric checkpoint/reclamation order.

DuckDB: ordered discovery does not imply safe early pruning

Pin 42d15ef8e0b84f3364cf507b40ebc37487299747; receipt (local evidence omitted from stage). src/optimizer/row_group_pruner.cpp recognizes a limited ORDER BY over a table scan. It rejects intervening additional limit operators and unsupported order expressions. Intervening filters/aggregation/distinct invalidate its row-limit bound; table filters specifically retain reordering while invalidating limit and offset pruning. This is a concrete guard against treating the first physical groups as enough qualifying rows. The selected file is an optimizer entrypoint, not inspection of all zonemap execution. Supplementary src/storage/table/row_group_reorderer.cpp tracks unresolved overlapping bounds: overlap may guarantee only one additional qualifying tuple, while non-overlap can admit a whole group's qualifying count. column_data.cpp::CheckZonemap checks base and update statistics and returns no pruning when their conclusions differ. Dynamic filters are not permanently marked checked. ZonemapScanEnd only skips vectors entirely within rejected segments and refuses that skip when updated rows may pass elsewhere. These safety cuts do not establish that a global match fraction suffices for early stopping.

src/storage/standard_buffer_manager.cpp obtains an eviction reservation before constructing a managed small buffer and transfers it to the block owner; unsuccessful eviction throws an out-of-memory error. This is explicit accounting for the managed buffer pool. It is not evidence that every process allocation or file-cache charge belongs to that pool.

Deduction: Fabric's existing source-bound Walk is a relevant baseline. Adding postings before measuring how much candidate work remains could duplicate a mechanism that already avoids rows. Full drains can erase a first-page advantage.

GreptimeDB: segment candidates and a live-memory accounting cut

Pin e4872ec1a2d0d040d9f05c370f238cd7f411328e; receipt (local evidence omitted from stage). src/index/src/bloom_filter/applier.rs maps candidate row ranges to unique sorted segment IDs, loads filters and applies AND across predicates with OR within each predicate's acceptable values. A matching filter produces whole segment ranges, then merges adjacent ranges and intersects with the original ranges. It does not return verified matching rows. An empty predicate list returns an empty result at this API; a caller cannot silently use that as an identity/all-rows rule. The file also handles a specific legacy missing all-null last segment; that compatibility branch is not a general corruption fallback contract.

src/index/src/bloom_filter/creator.rs hashes elements, deduplicates current segment hashes and finalizes on row boundaries. It deliberately inserts hashes individually rather than reserving for an iterator containing duplicate tokens. Its memory increments use new distinct hash count times size_of::<u64>(); that is narrower than actual HashSet capacity and allocator overhead.

src/index/src/inverted_index/create/sort/external_sort.rs stores copied values with Roaring postings over segment indices. Repeated values reuse a posting; sorting drains the HashMap into a Vec and sorts by value before external write. account_memory checks a global threshold and an optional local threshold. Crucially, dump_buffer takes the buffer and subtracts its global/current memory accounting before sorted(values) and awaited write_all consume the still-live postings. Observation: the ledger's scope ends before serialization ownership does. This is not a measured upstream bug; it is a counterexample to inferring live allocation from a sorter counter or copying its scope into a Fabric limit.

Tempo: ID lookup and general search take different paths

Pin b5a99aea3826ffc499827f2d0934ef80fc3d8b12; receipt (local evidence omitted from stage). The inspected tempodb/encoding/vparquet5/block_findtracebyid.go first reads a trace-ID-sharded Bloom filter. A positive result proceeds to an optional ID index, then opens Parquet and performs ID lookup. Disabled or missing ID index returns an unlocated candidate and uses the fallback; malformed/read-failed index returns an error. Bloom read/parse failures also return errors. Fabric's corrupt optional index rule requires exact fallback, so these error paths cannot be copied as-is.

The fallback binary-searches row-group bounds using the first TraceID value in each sampled group; the code explicitly relies on ascending TraceID order. It clones sampled ID bytes to detach them from I/O buffers. Fabric spans are ordered by time, identity, sequence and index: this physical premise cannot be assumed for Fabric's current spans table. Search across retained segments must also include every relevant span, not just locate one trace-shaped object.

tempodb/encoding/vparquet5/block_search.go uses dedicated-column schema changes, skips Parquet Bloom filters/page indexes on its general search open path and records backend bytes read. Its general Search obtains row groups with a comment that row-group predicate pushdown is not yet solved there. The ID-lookup mechanism therefore is not evidence that a general substring/tag query uses the same index. Supplementary index.go keeps the last ID added and appends it on a row-group flush; Find binary-searches those group upper bounds, not a posting for every span. create.go adds each trace ID to the index and Bloom filter alongside Parquet output. Complete finalizes index boundaries, closes/flushes Parquet, reads its footer to check magic/size, then hands metadata, Bloom and index to writeBlockMeta. This reveals coexisting build work, but that helper and backend durability were not selected. Build CPU, Bloom sizing and whole-path allocation remain unmeasured; compact ID boundaries require the sorted physical layout.

Roaring: shape and allocation depend on container boundaries

Pin 0439d576fb3d36acba4444f5a5bc0d4173ecd932; receipt (local evidence omitted from stage). roaring/src/bitmap/container.rs stores a u16 container key and selects Array, Bitmap or Run representations. Its array/bitmap cardinality threshold is 4096. optimize counts runs and compares serialized representation sizes before converting; the choice is not merely global bitmap density. Contiguous range construction can start with a Run. Thus equal cardinalities with different clustering and high-key spread can have different costs.

roaring/src/bitmap/ops.rs provides intersection cardinality without constructing the resulting bitmap. Borrowed set operations can allocate a container vector and clone retained containers, whereas owned operations can reuse ownership. Cardinality-only queries cannot supply Fabric's actual page rows or identities. The selected container code delegates lower-level storage operations; full operation costs are not established by an API signature. Supplementary roaring/src/bitmap/store/mod.rs dispatches cardinality intersections across Array/Array, Bitmap/Bitmap, Array/Bitmap, Run/Array, Run/Bitmap and Run/Run. Different layout pairs therefore have different kernels even at equal total cardinality. Lower-level implementations and runtime costs remain outside this selected-file inspection.

ClickHouse: decline saturated indexes and order pruning by cost

Pin 47907285810e618994aa703c31beb0edfc5d2271; failed initial retrieval receipt (local evidence omitted from stage) and the supplementary receipt above. The initial full-archive attempt remains curl exit 63, exit code 1. Bounded supplementary retrieval succeeded for Bloom .cpp/.h, Set, MinMax and MergeTreeDataSelectExecutor.cpp under src/Storages/MergeTree/; guessed separate MergeTreeIndexGranuleBloomFilter.cpp returned HTTP 404/exit 22 and is absent. This is selected implementation coverage, not a complete repository inventory.

MergeTreeIndexSet.cpp stops further set accumulation after exceeding its configured maximum distinct rows, serializes a zero-sized sentinel and returns mayBeTrue for zero/oversized granules. A bounded index losing discriminatory power deliberately becomes non-pruning instead of dropping evidence. For a usable set, it checks the hyperrectangle first, then evaluates expressions on stored values. Those expression costs count even when no rows are excluded.

MergeTreeIndexBloomFilter.cpp obtains distinct dictionary hashes for applicable LowCardinality columns and accumulates hash sets. Its Boolean-mask condition returns whether a granule can be true. It declines some indexed map comparisons against default values because an absent map key can return that same default. Candidate soundness depends on expression semantics, not just a mathematically sound Bloom filter. This selected generic Bloom path does not demonstrate arbitrary literal substring support.

MergeTreeIndexMinMax.cpp handles nullable extrema and relaxes conditions for columns whose extrema hide NaNs. MergeTreeDataSelectExecutor.cpp orders useful skip indexes per part by priority, coarser granularity and retained size: min/max precedes ordinary indexes, with conditional vector-index priority ahead of it. It caches those orders and records index-analysis statistics. This is a cost/order heuristic, not a measured optimum or an instruction to add every index. Accepted granules become merged mark ranges; exact row predicates still belong downstream. Missing/corrupt-index behavior was not established for every executor path and must not be assumed to match Fabric's exact fallback.

Four findings strengthened by the supplementary inspection

  1. Semantic applicability precedes candidate efficiency. Tantivy splits or slices text according to tokenizer rules; ClickHouse declines default-value map cases and relaxes NaN-sensitive bounds. Preserve Fabric's predicate before comparing candidate cost.
  2. Effective cardinality requires sound bounds and layout. DuckDB distinguishes overlapping bounds and updated statistics; Tempo stores sorted group boundaries. Neither licenses stopping after an estimated number of matches or using time-ordered addresses as ID-ordered bounds.
  3. An index may intentionally stop helping. ClickHouse saturated sets become non-pruning; Roaring selects different kernels by shape. A broader or high cardinality workload can favor exact scans/ranges.
  4. Maintenance ownership matters as much as lookup. Quickwit coordinates publication and checkpoint progress at its caller; Tempo builds Bloom/index state alongside Parquet. Greptime's observed accounting cut shows why build/serialization ownership overlap needs direct measurement.

Proposed discriminating trajectories

These are hypotheses, not registered runs. The existing Q1 census is diagnostic; its finite first-page/whole-process measurements are not index amortization or independent full-chain latency measurements. Freeze each later workload, state preservation bound, command and decision rule before execution.

Trajectory H1 and causal mechanism H0 / adverse case Small discriminating test and contract guard
Predicate candidates followed by exact verification A bounded trigram candidate representation avoids enough payload decode to beat existing Walk across repeated selective literal searches. Tantivy separates IDs from payload; Greptime returns candidate segment ranges. Dense/common terms, needles shorter than the candidate unit, or required integrity work dominate; a token index misses a substring. Compare existing filter/Walk with one scratch candidate on identical clustered and shuffled bodies, including punctuation, UTF-8 and broad matches. Count candidate rows, verified matches, false positives, hashed bytes and decode/output CPU. Grade every complete chain with the unchanged oracle; missing/corrupt candidates fall back exactly.
Build only where reuse pays Query lifetime savings can exceed candidate construction and publication cost for repeated narrow queries before expiry. Quickwit merger skip and scoped quotas motivate charging avoided maintenance too. Quiet/one-shot data, high cardinality or early retention makes the extra representation a net cost. Include zero, one and repeated query schedules over the same fixture; charge build CPU, temporary/retained bytes, query CPU and overlap peaks separately. Estimate a workload-specific break-even count instead of promoting a universal default. No adaptive policy is implemented by this test.
Shape-sensitive candidate sets with explicit ownership Sorted IDs/ranges outperform general bitmaps for sparse or clustered candidates, while Roaring wins another density/container regime; owned intersections reduce intermediate allocation. Conversion and container overhead erase gains at small size; borrowed results clone more than expected; sparse wide results pin decoded frames. Use equal-cardinality candidates with contiguous ranges, shuffled IDs and spread across container keys. Compare sorted IDs, ranges and scratch Roaring build/intersection/iteration including retained backing allocations. Exact row identity, ties and full output remain mandatory; cardinality equality alone is insufficient.
Trace lookup without changing primary order A secondary ID-to-time-ordered-row candidate map reduces exact span lookup cost without changing Fabric's time ordering. Tempo demonstrates an ID-specialized path but relies on a different physical order. Cross-segment trace scatter, full trace output, index build cost or snapshot mapping dominate; stale addresses omit spans after publication. Start with isolated scratch ID candidates for a time-ordered fixture, including one trace spread across Batches/Segments and equal-time ties. Compare against current span filters/Walk, charge exact predicate and output work, and grade full chains. Publication, retry identity and retention Gone controls precede any persistent mapping proposal.

The first three trajectories fit Q3 after Q1 attribution; the fourth depends on O2 snapshot/identity evidence. Do not nominate all four at once. The strongest current rejection is a blanket index or bitmap mandate: sources repeatedly separate predicate applicability, optional structures, physical order and owner scope. The next action is use measured Fabric costs to choose at most the existing bounded prototype allowance, retrieving deeper kernels only where that choice depends on them.

Inspection outcome and remaining uncertainty

Seven systems have selected implementation observations, including ClickHouse's bounded retry. Its failed full-archive attempt and guessed-path 404 stay recorded. Requested supplementary query files were inspected at unchanged pins; coverage remains local, with unselected lower-level kernels, metastore and backend durability outside the claim. No upstream test, Fabric build, benchmark, documentation validator or speed comparison ran in this inspection. No scratch files or processes were created; selected members were read in memory. Repository documentation is the only new artifact. The coordinator owns further bounded retrieval, source-manifest reconciliation, validation and registration.

⚠️ **GitHub.com Fallback** ⚠️