Skip to content

[feature-wip](inverted index) Introduce SPIMI V4 inverted index storage format#63633

Open
airborne12 wants to merge 67 commits into
apache:masterfrom
airborne12:inverted-index-spimi
Open

[feature-wip](inverted index) Introduce SPIMI V4 inverted index storage format#63633
airborne12 wants to merge 67 commits into
apache:masterfrom
airborne12:inverted-index-spimi

Conversation

@airborne12

@airborne12 airborne12 commented May 25, 2026

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:

Introduces a new inverted index storage format V4 powered by SPIMI
(Single-Pass In-Memory Indexing), replacing the CLucene IndexWriter
on the write path for analyzed (fulltext) string columns.

Why

CLucene's IndexWriter accumulates per-token Posting linked-list
nodes plus a term hash table plus a char[] interning pool. On Doris
fulltext columns this dominates BE memory during write and shows up
in OOM kills on large segments. The encoding is byte-equivalent to
Lucene 2.x, but the in-memory representation is the cost. SPIMI
keeps a flat (term_id, doc_id, position) record array plus a
single intern arena, then sorts + emits the same Lucene 2.x sibling
files (.tis/.tii/.frq/.prx/.fnm/segments_N) on finish(). The
on-disk format is unchanged; only the writer's working memory shape
changes.

Measured impact (SPIMI_BENCH=1, ~614 K occurrences/segment)

Dimension V4 vs V2 (mostly_unique / all_unique) V4 vs V2 (repetitive, vocab=16)
Writer peak memory −55.6 % / −55.6 % +406 % (160 KB → 811 KB; both negligible)
Writer CPU (median) −68 % / −68 % +5 % (within bench cap)
.idx on-disk size ~0 % +8 % (PFOR sub-block header overhead)
Query latency ~0 % (not measured at bench scale)

Repetitive vocab is the architectural trade-off region: V4's
compact-mode VInt-delta stream scales per-occurrence while CLucene's
Posting struct scales per-unique-term. Absolute memory in this
regime is sub-MB on both sides, so the percentage swing has no
production impact. Storage-size delta on repetitive is the
documented PFOR header cost.

What's in this PR

  • V4 writer pipeline (be/src/storage/index/inverted/spimi/):
    SpimiPostingBuffer (flat record + arena + intern map with
    hybrid compact-mode VInt-delta migration), SegmentWriter,
    TermDictWriter, FieldInfosWriter, SegmentInfosWriter,
    PFOR encoder for high-doc-freq postings, ByteOutput family
    abstracting CLucene's IndexOutput.
  • V4 reader pipeline: SpimiQueryIndexReader,
    SpimiTermDocsReader, SpimiProxReader, SpimiTermEnum,
    SpimiSearcherBuilder; SpimiFulltextIndexReader is the
    Doris-side adapter (overrides type() -> SPIMI_FULLTEXT so
    the searcher cache routes correctly).
  • column_reader.cpp dispatch: V4 storage format → SPIMI
    reader; V1/V2/V3 unchanged.
  • EmitSegment post-flush self-validation:
    ValidateClosedSegmentByteCounts re-queries on-disk file
    lengths after close, throws INVERTED_INDEX_FILE_CORRUPTED on
    mismatch — guards against the async-S3 partial-flush class of
    bugs that single-node tests can't see.
  • 108 BE unit tests under be/test/storage/index/inverted/spimi/
    plus extended tests under be/test/storage/segment/:
    • 17 corruption-path tests covering every SPIMI_THROW_CORRUPT
      site (segments_N / .frq / .prx / PFOR / .tis-.tii readers)
    • 7 byte-count validator tests including the truncation
      fault-injection case
    • Storage-size benchmark (V2 vs V4 .idx byte parity)
    • Throughput benchmark with 11 runs + 2 warmup discards +
      randomized V2/V4 alternation + full distribution report
    • Memory benchmark across mostly_unique / all_unique /
      repetitive workloads
    • Query-latency benchmark via the production read path
      (InvertedIndexReaderTest.SpimiV2V4QueryLatencyBenchmark)
      using the corrected SpimiFulltextIndexReader::create_shared
      dispatch
  • SPIMI_BENCH env-var tier: default UT runs use 12 K
    occurrences (fast regression guard); SPIMI_BENCH=1 scales to
    ~614 K, SPIMI_BENCH=large scales to ~6 M for full-segment
    stress. Keeps headline benchmark numbers reproducible without
    ballooning every UT pass.
  • Regression suites:
    • inverted_index_p0/storage_format/test_storage_format_v4
      — V2 vs V4 black-box parity across MATCH_ANY / MATCH_ALL /
      MATCH_PHRASE / MATCH_PHRASE_PREFIX / MATCH_REGEXP, NULL/empty
      handling, and the support_phrase=false (omit_tfap) no-prox
      write+read path.
    • test_storage_format_v4_cloud — same coverage gated by
      isCloudMode() so the async-S3 upload path gets exercised.
    • test_storage_format_v4_query_latency — cluster-level
      V2 vs V4 query timing distribution.
  • FE plumbing (PropertyAnalyzer, TabletIndex,
    OlapTable): accept inverted_index_storage_format=V4 in
    CREATE TABLE PROPERTIES; propagate through the protocol to BE.

What's NOT in this PR (known gaps)

  • V4 segment compaction across multiple SPIMI segments — V4
    currently emits a single _0 segment per column; compaction is
    documented as a follow-up in SPIMI_DESIGN.md.
  • BM25-style scoring on V4 — V4 sets omit_norms=true; the read
    side synthesizes a default-norm array. Score-using paths
    (MATCH_ALL with relevance ordering) fall back to V2 behavior
    on V4 columns. Listed in design doc.
  • V4 only covers analyzed (fulltext) string columns. Keyword-mode
    (should_analyzer=false) and numeric (BKD) paths remain on the
    existing writers.

Release note

Add inverted index storage format V4, an in-house SPIMI-based writer
that reduces BE write-side memory by ~55 % and CPU by ~68 % on
diverse-vocab fulltext workloads while keeping segment on-disk
format Lucene 2.x compatible. Enable by setting
inverted_index_storage_format = "V4" in CREATE TABLE PROPERTIES.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes. New value 'V4' accepted by inverted_index_storage_format property; V1/V2/V3 paths unchanged.
  • Does this need documentation?

    • No.
    • Yes. Doc PR will follow against apache/doris-website.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

gavinchou
gavinchou previously approved these changes May 25, 2026

@gavinchou gavinchou left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@airborne12 airborne12 force-pushed the inverted-index-spimi branch from 9281771 to 0f96cfa Compare May 26, 2026 03:22
@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 78.06% (1854/2375)
Line Coverage 64.50% (33323/51665)
Region Coverage 65.23% (16530/25343)
Branch Coverage 55.72% (8834/15854)

@airborne12 airborne12 changed the title [feature](be)(fe) Introduce SPIMI V4 inverted index storage format [feature](inverted index) Introduce SPIMI V4 inverted index storage format May 26, 2026
@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 0.00% (0/8) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 1.69% (2/118) 🎉
Increment coverage report
Complete coverage report

2 similar comments
@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 1.69% (2/118) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 1.69% (2/118) 🎉
Increment coverage report
Complete coverage report

@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@airborne12

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found blocking correctness issues in the V4 SPIMI spill path and validation gaps that can expose unsupported V4 tables.

Critical checkpoint conclusions:

  • Goal/test proof: the PR wires V4 SPIMI write/read and adds broad tests, but the tests do not cover the production multi-spill case with absolute row ids and include a timing assertion that is likely flaky in CI.
  • Scope/focus: mostly focused on V4 SPIMI, but the new p0 latency benchmark is not a stable functional test.
  • Concurrency/lifecycle: no new shared concurrent state issue identified in the reviewed path; cache-reader lifetime changes appear to use existing searcher cache patterns.
  • Configuration/compatibility: V4 is exposed through table properties/config and thrift/proto, but FE validation still permits unsupported V4 index shapes that BE later rejects during load.
  • Parallel paths: cloud/non-cloud schema propagation has V4 branches, but unsupported parser/array paths need consistent FE rejection.
  • Data correctness: multi-spill merging can corrupt posting doc ids, and flush can persist a stale doc_count for the triggering row. These affect query correctness for larger V4 fulltext segments.
  • Test coverage/results: there are many unit/regression tests, but the missing absolute-doc-id multi-spill coverage lets the main spill corruption escape; the latency regression should not be p0 threshold-based.
  • Observability/performance: no additional observability blocker found beyond the functional issues above.

User focus: no additional user-provided focus points were present.

// Apply doc_id offset and append.
for (auto& d : docs) {
d.doc_id += offset;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This offset corrupts production V4 spills. InvertedIndexColumnWriter::add_values() appends the segment-level _rid into SpimiPostingBuffer, and SpillManager::FlushBuffer() emits those doc ids as-is, so spill inputs already contain absolute row ids. When a large segment crosses the SPIMI memory budget more than once, the second and later spills get running added again here, shifting postings beyond their real rows (often beyond total_doc_count) and causing MATCH queries to return wrong/missing rows. The current merger tests build artificial inputs with local doc ids, so they do not cover the actual writer->spill contract. Please either make spill buffers localize doc ids before emitting, or remove this offset for V4 spill inputs and add an end-to-end test with multiple spills using absolute _rid values.

// and continue accepting tokens.
if (_spimi_writer->ShouldFlush()) {
_spimi_writer->FlushPending(_spimi_doc_count);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc count passed to the spill is stale for the row that triggered the flush. At this point the current row's tokens have already been appended with doc id _rid, but _spimi_doc_count is only advanced below this block. If this is the first row past the budget, the spill manifest can advertise a doc_count that excludes a doc whose postings are present. That stale count is then used by SegmentMerger for offsets/metadata. Update _spimi_doc_count before FlushPending() (or pass static_cast<int32_t>(_rid) + 1) so each spill's metadata covers all postings it contains.

@@ -1275,11 +1277,15 @@ public static TInvertedIndexFileStorageFormat analyzeInvertedIndexFileStorageFor
return TInvertedIndexFileStorageFormat.V2;
} else if (invertedIndexFileStorageFormat.equalsIgnoreCase("v3")) {
return TInvertedIndexFileStorageFormat.V3;
} else if (invertedIndexFileStorageFormat.equalsIgnoreCase("v4")) {
return TInvertedIndexFileStorageFormat.V4;
} else if (invertedIndexFileStorageFormat.equalsIgnoreCase("default")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposing explicit V4 here needs matching FE validation for the supported index shapes. The BE writer now rejects V4 when should_analyzer is false and also rejects array string indexes, but InvertedIndexUtil.checkInvertedIndexParser() still accepts parser none and array-with-parser-none for the same table property. That lets CREATE TABLE ... PROPERTIES("inverted_index_storage_format"="V4") succeed for keyword/array inverted indexes, and the first load then fails in BE writer init. Please reject unsupported V4 parser/type combinations during analysis, or implement those paths end-to-end.

// on top of the actual reader latency. A real reader regression
// would shift the median far past this cap.
assertTrue(ratio < 2.0,
"${tag}: V4 median ${v4.median} us / V2 median ${v2.median} us = ${ratio} " +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A p0 regression suite should not fail on wall-clock query timing. This median ratio includes planner/executor startup, cache state, BE scheduling, network/runner noise, and unrelated concurrent load; on shared CI it can exceed 2x without a functional regression, especially with only 9 retained samples. This will create flaky failures for unrelated changes. Please keep this as logging/manual benchmark coverage, move it out of p0/per-PR execution, or replace the assertion with deterministic functional checks.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 0.00% (0/8) 🎉
Increment coverage report
Complete coverage report

@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 78.06% (1854/2375)
Line Coverage 64.53% (33337/51663)
Region Coverage 65.19% (16522/25343)
Branch Coverage 55.75% (8838/15854)

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 31767 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 3eae47c6fefd24b24b0604c82634bf4898004b70, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17796	4176	4206	4176
q2	q3	11004	1483	831	831
q4	4803	476	347	347
q5	10511	2272	2127	2127
q6	391	189	144	144
q7	1006	790	652	652
q8	9607	1737	1578	1578
q9	7064	5027	5015	5015
q10	6622	2246	1855	1855
q11	438	279	253	253
q12	697	430	303	303
q13	18191	3503	2823	2823
q14	265	260	245	245
q15	q16	822	774	709	709
q17	902	954	923	923
q18	7077	5731	5533	5533
q19	1201	1406	1174	1174
q20	507	402	263	263
q21	5682	2597	2505	2505
q22	430	366	311	311
Total cold run time: 105016 ms
Total hot run time: 31767 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	4545	4419	4515	4419
q2	q3	4588	4991	4305	4305
q4	2158	2228	1397	1397
q5	4483	4979	4997	4979
q6	272	206	149	149
q7	2051	1859	1663	1663
q8	2684	2367	2283	2283
q9	8141	8061	8252	8061
q10	4889	4770	4406	4406
q11	628	447	412	412
q12	746	771	542	542
q13	3335	3752	3082	3082
q14	322	315	292	292
q15	q16	763	719	655	655
q17	1399	1363	1356	1356
q18	8098	7373	6985	6985
q19	1143	1093	1071	1071
q20	2221	2228	1946	1946
q21	5349	4716	4494	4494
q22	565	476	407	407
Total cold run time: 58380 ms
Total hot run time: 52904 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 172092 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 3eae47c6fefd24b24b0604c82634bf4898004b70, data reload: false

query5	4318	660	527	527
query6	343	250	213	213
query7	4249	540	331	331
query8	335	245	229	229
query9	8812	4024	4033	4024
query10	465	361	296	296
query11	5740	2552	2235	2235
query12	185	131	128	128
query13	1294	600	449	449
query14	6096	5474	5152	5152
query14_1	4513	4524	4511	4511
query15	216	205	189	189
query16	1044	478	459	459
query17	1174	776	619	619
query18	2788	509	372	372
query19	228	205	186	186
query20	135	133	132	132
query21	219	139	117	117
query22	13692	13570	13386	13386
query23	17442	16636	16285	16285
query23_1	16240	16395	16410	16395
query24	7521	1775	1332	1332
query24_1	1346	1311	1344	1311
query25	582	497	455	455
query26	1316	314	174	174
query27	2681	574	344	344
query28	4392	2007	2009	2007
query29	1000	647	526	526
query30	314	246	199	199
query31	1130	1095	977	977
query32	103	79	82	79
query33	571	360	302	302
query34	1210	1129	656	656
query35	815	795	709	709
query36	1378	1413	1274	1274
query37	169	106	95	95
query38	3194	3209	3063	3063
query39	932	920	907	907
query39_1	900	889	873	873
query40	234	151	119	119
query41	66	62	61	61
query42	109	110	106	106
query43	321	338	290	290
query44	
query45	216	200	201	200
query46	1105	1205	742	742
query47	2410	2388	2234	2234
query48	368	434	302	302
query49	633	492	377	377
query50	1014	357	251	251
query51	4406	4316	4283	4283
query52	100	104	94	94
query53	246	282	200	200
query54	310	263	244	244
query55	92	92	88	88
query56	288	308	305	305
query57	1477	1408	1362	1362
query58	291	265	262	262
query59	1553	1644	1404	1404
query60	314	319	301	301
query61	165	164	164	164
query62	683	659	603	603
query63	251	203	201	201
query64	2361	788	627	627
query65	
query66	1672	478	353	353
query67	29734	29824	29450	29450
query68	
query69	452	367	305	305
query70	1018	1013	1007	1007
query71	309	276	266	266
query72	2992	2706	2398	2398
query73	859	774	420	420
query74	5092	4961	4751	4751
query75	2698	2631	2270	2270
query76	2305	1147	792	792
query77	406	413	334	334
query78	12442	12366	11808	11808
query79	1518	1002	732	732
query80	1347	540	460	460
query81	495	285	241	241
query82	1343	156	118	118
query83	349	267	251	251
query84	270	147	113	113
query85	930	542	471	471
query86	449	332	351	332
query87	3432	3418	3267	3267
query88	3631	2732	2724	2724
query89	459	384	345	345
query90	1917	179	181	179
query91	177	170	144	144
query92	77	80	76	76
query93	1549	1428	865	865
query94	754	373	321	321
query95	674	370	335	335
query96	1053	757	338	338
query97	2732	2738	2621	2621
query98	238	228	228	228
query99	1184	1157	1040	1040
Total cold run time: 255573 ms
Total hot run time: 172092 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 25.00% (2/8) 🎉
Increment coverage report
Complete coverage report

@airborne12 airborne12 force-pushed the inverted-index-spimi branch from 3eae47c to b334f74 Compare June 2, 2026 15:38
…h() catch-all

Addresses two review findings on the SPIMI V4 write/read path:

1. query_term_enum.cpp Cursor::ReadVInt/ReadVLong left-shifted `b << shift`
   with no bound on `shift`. A crafted/corrupt .tis with >=5 (VInt) / >=10
   (VLong) continuation bytes drives shift past the operand width (>=32 / >=64)
   — undefined behavior. Mirror the existing `shift >= 32U/64U` guards from
   term_dict_reader.cpp; throw CLuceneError(CL_ERR_IO), which the query path
   already converts to INVERTED_INDEX_FILE_CORRUPTED. Adds a corrupt-input
   regression test (CorruptVIntShiftOverflowThrowsNotUB).

2. InvertedIndexColumnWriter::finish() caught only CLuceneError and
   doris::Exception around the SPIMI emit. SpimiIndexWriter::Finish() rethrows
   the original exception type (e.g. a std::bad_alloc), so an unlisted type
   escaped past the FINALLY block — leaking the seven SPIMI IndexOutputs and
   skipping the eptr->Status conversion. Add a catch-all that funnels any
   exception through the same error_context path so cleanup runs uniformly and
   the FINALLY macro converts it to an INVERTED_INDEX_CLUCENE_ERROR Status.

Verified locally under ASAN+DCHECKs: 252 passed / 0 failed (5 env-gated skips).
@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 0.00% (0/8) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28938 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit bd88a847b21adc1430b00ed5391ae2136f0fe69b, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17693	4094	4044	4044
q2	q3	10946	1430	809	809
q4	4794	486	350	350
q5	8611	892	591	591
q6	361	179	139	139
q7	922	853	648	648
q8	10959	1559	1626	1559
q9	7108	4513	4503	4503
q10	6842	1812	1530	1530
q11	447	274	258	258
q12	655	431	291	291
q13	18148	3361	2806	2806
q14	272	264	249	249
q15	q16	826	778	714	714
q17	988	981	919	919
q18	7447	5637	5616	5616
q19	1177	1372	1017	1017
q20	507	413	257	257
q21	5592	2542	2329	2329
q22	440	356	309	309
Total cold run time: 104735 ms
Total hot run time: 28938 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	4359	4318	4245	4245
q2	q3	4496	4921	4290	4290
q4	2118	2203	1373	1373
q5	4423	4333	4276	4276
q6	228	179	228	179
q7	2214	1932	1706	1706
q8	2486	2110	2147	2110
q9	7983	7870	8002	7870
q10	4819	4787	4596	4596
q11	562	422	394	394
q12	765	746	534	534
q13	3220	3789	3035	3035
q14	301	314	279	279
q15	q16	742	777	640	640
q17	1345	1359	1327	1327
q18	7846	7216	6818	6818
q19	1140	1116	1123	1116
q20	2216	2224	1955	1955
q21	5267	4564	4424	4424
q22	524	466	417	417
Total cold run time: 57054 ms
Total hot run time: 51584 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 170535 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit bd88a847b21adc1430b00ed5391ae2136f0fe69b, data reload: false

query5	4392	639	476	476
query6	448	203	188	188
query7	4830	589	304	304
query8	377	219	213	213
query9	8753	4090	4051	4051
query10	454	329	301	301
query11	5922	2355	2174	2174
query12	157	101	98	98
query13	1277	594	433	433
query14	6427	5569	5260	5260
query14_1	4592	4582	4602	4582
query15	214	205	179	179
query16	1033	496	442	442
query17	1161	730	581	581
query18	2724	472	346	346
query19	200	185	145	145
query20	113	110	110	110
query21	214	147	123	123
query22	13626	13575	13367	13367
query23	17292	16580	16261	16261
query23_1	16444	16349	16416	16349
query24	7603	1794	1303	1303
query24_1	1332	1324	1313	1313
query25	565	460	396	396
query26	1306	315	170	170
query27	2664	596	326	326
query28	4426	2009	2021	2009
query29	1058	653	499	499
query30	319	240	199	199
query31	1120	1075	990	990
query32	104	63	60	60
query33	523	325	255	255
query34	1185	1182	657	657
query35	774	797	731	731
query36	1381	1368	1247	1247
query37	154	103	97	97
query38	3245	3163	3101	3101
query39	935	935	899	899
query39_1	909	888	862	862
query40	228	135	109	109
query41	72	70	71	70
query42	96	97	97	97
query43	334	347	295	295
query44	
query45	202	196	186	186
query46	1110	1212	754	754
query47	2408	2400	2321	2321
query48	394	426	303	303
query49	658	498	369	369
query50	1027	394	287	287
query51	4420	4383	4271	4271
query52	91	93	83	83
query53	251	279	196	196
query54	309	233	236	233
query55	83	79	73	73
query56	279	263	253	253
query57	1458	1412	1336	1336
query58	253	218	225	218
query59	1654	1701	1505	1505
query60	302	274	251	251
query61	191	191	187	187
query62	714	656	581	581
query63	238	190	191	190
query64	2617	829	701	701
query65	
query66	1794	463	350	350
query67	29761	29651	28954	28954
query68	
query69	433	310	264	264
query70	995	994	955	955
query71	323	223	220	220
query72	3036	2507	2459	2459
query73	874	791	410	410
query74	5183	4973	4777	4777
query75	2681	2572	2242	2242
query76	2354	1182	795	795
query77	357	387	292	292
query78	12478	12556	11831	11831
query79	1421	1020	787	787
query80	1324	473	409	409
query81	527	281	244	244
query82	573	156	120	120
query83	365	286	263	263
query84	
query85	945	561	437	437
query86	432	296	304	296
query87	3452	3360	3213	3213
query88	3702	2757	2777	2757
query89	450	387	332	332
query90	1939	192	182	182
query91	179	158	140	140
query92	62	60	57	57
query93	1477	1473	872	872
query94	717	359	318	318
query95	678	473	357	357
query96	1029	771	370	370
query97	2715	2718	2601	2601
query98	214	211	202	202
query99	1178	1187	1036	1036
Total cold run time: 253654 ms
Total hot run time: 170535 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 83.15% (5193/6245) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 74.10% (28721/38758)
Line Coverage 58.24% (313627/538543)
Region Coverage 54.95% (262558/477770)
Branch Coverage 56.31% (113969/202407)

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 3.12% (2/64) 🎉
Increment coverage report
Complete coverage report

SegmentMerger::Merge applied a per-segment doc_id offset (sum of prior doc_counts), assuming local 0-based inputs. But the write path appends the absolute _rid and FreqProxEncoder::StartTerm resets _last_doc to 0, so each spill segment already holds GLOBAL absolute doc_ids that never overlap across inputs. The offset double-shifted every doc after the first spill, silently corrupting the merged segment's row mapping and pushing doc_ids past total_doc_count for any segment that crossed the 256MiB budget or spilled under memory pressure.

Existing multi-input merger tests fed local-0-based segments with local doc_counts (a self-consistent combination) so the bug was invisible. The merge now concatenates the already-ordered absolute-id runs verbatim (no offset); removes the unused MergeCursor; adds MultiSpillAbsoluteDocIdsArePreserved reproducing the production contract (SpillManager double spill + cumulative doc_count + read-back); and rewrites the multi-input tests to construct absolute, non-overlapping doc_ids.
V4 inverted index segments are not byte-compatible with CLucene's indexCompaction/mergeTerms; feeding a V4 .idx to it reads past EOF and SIGSEGVs the whole BE during background index compaction. Skip byte-level index compaction for V4 tablets in construct_index_compaction_columns; the index columns then fall through to the normal compaction rewrite path (segment_writer rebuilds the SPIMI index from merged column data, skip_inverted_index stays false), which is correct and crash-safe. Native SPIMI segment merging during compaction is a follow-up.
@airborne12 airborne12 requested a review from luwei16 as a code owner June 11, 2026 14:58
…ock heap alloc

pack_bits allocated a heap vector per 128-value block (a large segment emits millions of blocks); select_patch copied + fully std::sort'd the block only to read one order statistic (the exception threshold); EncodeBlock's low[] was another per-block heap vector. Replace all three with stack buffers (count<=128, width<=32 bound the payload to <=520 B) and nth_element for the single quantile read. Output is byte-identical — WindowFrameEncoderTest.ByteIdentityGolden, the PFOR differential test, and segment roundtrip all stay green — while dropping 3-5 malloc/free per block on the .frq/.prx encode hot path.
…ort, direct buffer calls

Three byte-identical write-path wins (ByteIdentityGolden + roundtrip + V4 writer/merger tests green, 127 pass):

* HashTerm: replace per-byte FNV-1a (16-22% of V4 wall-clock per the flame graph) with an 8-byte-at-a-time rapidhash-style mum mix. The per-instance seed still seeds the state and is folded by every mum step (C5 DoS resistance) and the hash stays fully deterministic; it only indexes the in-memory slot table (terms emit in lexicographic arena order) so on-disk bytes are unchanged.

* Term sort: precompute a big-endian 8-byte prefix key so std::sort resolves most comparisons with one integer compare instead of two ArenaTermAt lookups + a memcmp; a first-8-byte tie falls back to the full arena compare. Sort order, hence emitted bytes, unchanged.

* add_values V4 loop: fetch the posting buffer once and call Append/Saturated directly, dropping a cross-TU facade hop per token (BE builds without LTO); Saturated then inlines to a single load.
Disabling .frq ZSTD gives +17~38% write throughput on short/medium-doc corpora (V4 reaches parity with V2): the adaptive-W search no longer compresses every candidate framing. Disk cost is bounded — 73-81% of the ZSTD saving lives in .prx, which stays compressed — so most corpora are flat or smaller; httplogs-class short ASCII pays +10.7% .idx. The .prx framing is already decoupled from the .frq gate, so a raw .frq no longer fragments .prx into incompressible windows. ByteIdentityGolden now also pins frq_zstd_enable=true (alongside the existing zstd_min pin) so it keeps locking the full-ZSTD output regardless of the production default.

Decision confirmed by the user after reviewing the throughput/disk trade-off.
With spill-to-disk (spilled bytes are freed to a node-local tmp file, no resident accumulation), a lower budget sharply cuts the large-document write peak — wiki goes 546->259 MB, below V2's 288 MB — for only +0.75% .idx (more spills make the k-way merge re-encode split terms slightly less optimally). Short documents never cross the budget and are unaffected (no spill, peak ~= buffer). memory_budget_test asserts per-record/arena bounds independent of the budget value, so it stays green.

Decision confirmed by the user after reviewing the memory/disk trade-off.
…spills

MergeSingleInput already byte-copies a single phrase-on spill's .tis/.tii/.frq/.prx and rebuilds only metadata. The omit_term_freq_and_positions guard that excluded DOCS_ONLY single spills was a leftover from before the spill omit flag was lockstepped with the output: a DOCS_ONLY spill is written omit=true (doc-only .frq, empty .prx) in exactly the format the output advertises, so the byte-copy is equally valid — it copies the empty .prx verbatim and rebuilds .fnm with has_prox=false. Dropping the guard lets a DOCS_ONLY segment flushed exactly once skip the decode/re-encode merge (~40-50% of that path's write cost). Renamed the test to fast-path and asserted byte-copy equivalence (output .tis/.frq == input, empty .prx).

ASAN: SegmentMergerTest + roundtrip 40 pass / 0 fail.
The in-memory prox chain stored each occurrence's ABSOLUTE position. For long documents an absolute position needs 2-3 VInt bytes while the intra-doc gap is usually 1, so the chain was larger than necessary (the prox pool is one of the largest buckets in compact mode). Store the within-doc delta instead (Lucene-style): last_pos resets to 0 at each doc boundary, the first position in a doc is stored as itself and the rest as gaps. Both decode paths (EmitFromCompactDirect, DecodeCompactTerm/DecodeTermToRecords) prefix-sum the deltas back to absolute before re-encoding, so the on-disk .prx — and the records[] consumed by the norms/materialized path — stay byte-identical. Modular subtraction round-trips any order, matching the doc-delta scheme. DOCS_ONLY is unaffected (no prox chain). Cuts phrase-on peak write memory ~10-20% on long-doc corpora.

ASAN: segment roundtrip (positions prefix-sum) + posting-buffer decode (records.position 1,3,5,9) + ByteIdentityGolden + writer/merger, 127 pass.
… sort

Two byte-identical follow-ups to the write hot-path series (ByteIdentityGolden + roundtrip + merger/writer suites, 176 pass):

* EncodeBlock declared 'values may be modified' but the implementation is read-only (max scan + masked copy into a stack low[]); the non-const signature forced EncodePforPart to scratch-copy every 128-value sub-block before encoding, and EncodeBlockToBytes to copy its whole input. Make the parameter const and feed slices of the staging arrays directly.

* The flat-mode counting-sort stage-1 (distinct text_refs) used the same double-ArenaTermAt+memcmp comparator the compact path had; give it the same big-endian 8-byte prefix key so most comparisons are one integer compare.
…ake it production-shaped

config::init's default-application loop aborts (returns false) on the first field whose default needs an unresolvable env substitution — custom_config_dir = "${DORIS_HOME}/conf" — so without DORIS_HOME every config registered after it (ALL inverted_index_* knobs) stayed zero-initialized: prx_zstd=0, zstd_min_window_bytes=0, prx_window_docs=0 (pathologically finest .prx windows), ram_dir=0, and spill_check_interval_rows=0, which clamps to 1 and made V4 run its expensive memory-watermark gate EVERY ROW (production: every 512). The benchmark had been measuring a config unlike production on both axes that matter most for the V2/V4 comparison. The swallowed init failure is now a LOG(FATAL), DORIS_HOME is provided, and a [BENCH-CONFIG] provenance line logs every measurement-affecting knob per run.

Also production-shapes the harness against cluster-E2E discrepancies: per-dataset row caps (SPIMI_*_ROWS; wikipedia ignores the global SPIMI_BENCH_ROWS so a 200K short-doc cap can't inflate it into 5 GB segments), Doris per-alloc memory tracking ON by default (SPIMI_TRACK_MEM=0 restores the old untracked mode), opt-in write-time searcher-cache warmup (SPIMI_WARMUP_CACHE=1, matching CI/cluster confs), production data-page-sized add_values batches (4096, was 32), and concurrent 16/32-thread V2-vs-V4 cases on the real textbench/weibo corpora — the E2E-shaped headline metric (the historical cluster 'V4 1.5x slower' was allocator contention invisible to single-thread benchmarks).
… pos offsets, analytic raw-W search

Four byte-identical encoder wins, validated by ByteIdentityGolden + roundtrip + merger/writer suites (128 pass) AND a benchmark idx byte-equality check (all six corpus/mode idx sizes exactly unchanged):

* SLIM terms (df < skip_interval, the vocabulary long tail) now write their pure-VInt block straight to the block sink instead of staging through _frq_term_buf and copying at FinishTerm — the slim block is never ZSTD-wrapped, so staging bought nothing. The 0-level skip tail writes no bytes and slim's skip_pointer return is discarded, so output is unchanged.

* ComputeNumberOfSkipLevels short-circuits df < skip_interval (floor(log ratio) is provably 0), saving two std::log calls per slim term; FlushFrqBlock/FlushProxBlock construct their faststring compression scratch inside the compression branch so slim/tiny terms never pay it.

* The windowed encoder records each doc's position-byte offset at StartDoc (when the boundary is known for free) and passes it to WindowFrameEncoder::Encode, which previously recovered doc boundaries by re-scanning the ENTIRE position VInt stream byte-by-byte per term — .prx is the largest stream, so that was a full extra pass over most of the index. The redundant _win_pos_counts vector (always == _win_freqs) is gone; the offset-less scan survives as the fallback for direct Encode callers (tests).

* With .frq ZSTD disabled (the production default), every window payload is the raw tuple, so the adaptive-W search's candidate sizes are pure arithmetic over unit part lengths. AnalyticRawFrqSize mirrors MeasureAndCacheFrq's accounting term for term; the search now computes candidate sizes analytically and composes ONLY the chosen framing, removing the baseline + per-candidate full-term byte copies. A per-term DCHECK cross-checks analytic == measured for the composed framing (held across the whole ASAN suite), and the benchmark idx equality confirms the W decision is bit-identical end to end.
… contract

MergerInlineOutputMatchesExternal still built its two merge inputs with LOCAL per-input doc ids ({1,2} each), relying on the per-segment offset the merger no longer applies (the offset was removed when the multi-spill doc-id corruption was fixed: production inputs carry GLOBAL absolute ids). Under the absolute contract the two inputs overlapped, tripping StartDoc's strictly-ascending DCHECK. Rebuild the inputs as successive slices of one absolute stream ({1,2} then {4,5}, cumulative doc_counts); the reference segment's expectations were already expressed in global ids and are unchanged. This was the one multi-input merge fixture outside spill_segment_merger_test.cpp that the doc-id fix missed.
…copy fast path)

For a slim phrase-on term (df < skip_interval — the long tail that dominates a real vocabulary), the posting buffer's freq chain bytes ARE the on-disk slim .frq block (per-doc docCode VInts: same values in the same order; VInt64/VInt of the same value encode identically and direct-emit input is monotonic), and since the buffer stores within-doc position deltas, the prox chain bytes ARE the raw .prx payload FlushProxBlock builds. EmitFromCompactDirect now copies each chain with one memcpy per slice (ByteSliceReader::AppendRemainingTo) and emits it pre-encoded (FreqProxEncoder::EmitSlimTermPreEncoded, sharing FlushProxRaw for the .prx mode-byte + ZSTD policy), replacing the per-occurrence VInt decode -> encoder state machine -> re-encode replay. DOCS_ONLY stays on the replay (its chain carries freq codes the bare-delta on-disk format omits).

Byte-identity: the existing CompactDirectEmitMatchesRecordsPathVInt A/B already locks the non-inline branch; the new CompactDirectEmitMatchesRecordsPathV4Inline locks the V4 (windowed + inline) configuration across the inline (df=3), slim-boundary (511) and windowed (600) cases against the records-path replay. Full SPIMI-wide ASAN suite 486 pass / 0 fail; benchmark idx sizes byte-equal on all six corpus/mode cells.
…ms chain-copy

The DOCS_ONLY posting chain stored phrase-shaped docCode VInts (delta<<1|flag [+freq]) that the emit only decoded to throw the freq away and re-encode bare deltas. Store the bare doc-delta directly — written EAGERLY at doc open (no deferred close; FinalizeBlocks skips omit buffers), one entry per doc, which is byte-for-byte the on-disk DOCS_ONLY slim block. Slim omit terms (df < skip_interval) then take the same chain-copy fast path as phrase-on terms (EmitSlimTermPreEncoded grows omit support: no prox bytes, prox_pointer=0); PFOR omit terms replay doc_count bare deltas. The chain also shrinks in memory (no freq codes), trimming the DOCS_ONLY buffer.

Format ownership: the chain format follows the BUFFER's omit flag, the on-disk format follows the WRITER's — chain-copy and the bare-delta replay are gated on the flags AGREEING. The one legal mixed combination (omit writer over a phrase buffer, which tests use to emit DOCS_ONLY from a generic buffer) keeps the docCode decode+re-encode replay; the OmitTfapByteNeutral A/B pair caught the first cut keying this off the writer flag alone.

Omit records semantics: DecodeCompactTerm/DecodeTermToRecords now yield one record per DOC for omit buffers (per-occurrence multiplicity is not recoverable from a bare-delta chain, and nothing downstream needs it — the omit emit ignores freq). Norms are the only per-occurrence consumer, and omit fields never write norms in V2/CLucene either; EmitSegment grows a DCHECK so a future norms-over-omit caller fails loudly.

Validated: SPIMI-wide ASAN suite 486 pass / 0 fail, including the OmitTfapByteNeutral{VInt,Pfor} byte-equality oracles, OmitTfapDirectEmitMatchesRecordsPath, and the DOCS_ONLY roundtrip.
…, last-term swap

Three byte-identical term-dictionary wins, one per term across the whole vocabulary: Utf8ToWideInto fills a reused member wstring instead of heap-allocating one per Add/AddInline; the front-coded suffix is staged once (AppendSCharsFromWide, sharing one EncodeSChar core with WriteSCharsFromWide so the encodings cannot drift) and emitted with a single bulk WriteBytes instead of a virtual WriteByte per encoded byte; and the .tis last-term update swaps the scratch instead of copying the wstring (the .tii boundary entries, 1 in 128, still copy and never reach the swap branch). SPIMI-wide ASAN suite 486 pass / 0 fail.
…d emit)

The windowed-term replay decoded every position from the prox chain (prefix-summing within-doc deltas to absolutes) only for AddPosition to recompute the SAME deltas and re-encode the SAME LEB128 bytes into the windowed position buffer. Since the chain bytes and the rebuilt buffer are byte-identical, EmitFromCompactDirect now copies the whole prox chain once (one memcpy per slice), decodes only the per-doc docCode entries (df values — cheap relative to occurrences), recovers each doc's position byte offset with a continuation-bit scan (no value decode), and hands everything to FreqProxEncoder::EmitWindowedTermPreDecoded — which produces exactly FinishTermWindowed's output via the same WindowFrameEncoder::Encode call. High-frequency terms (wiki-class head words, millions of occurrences) skip the per-occurrence decode + re-encode entirely.

Gated on V4 windowed + phrase-on with a phrase BUFFER (the chain must carry positions); the omit-writer-over-phrase-buffer combination keeps the replay. SPIMI-wide ASAN suite 486 pass / 0 fail, including the V4Inline direct-vs-records A/B's windowed (df=600) case.
@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29465 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 4006aee3a4cc7b9112c427bbf83d5e175d9d4dab, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17734	4108	4038	4038
q2	q3	10998	1467	819	819
q4	4809	476	345	345
q5	8617	894	577	577
q6	362	174	140	140
q7	911	870	639	639
q8	10906	1470	1677	1470
q9	7176	4497	4546	4497
q10	6804	1820	1521	1521
q11	447	266	259	259
q12	657	433	297	297
q13	18199	3414	2824	2824
q14	272	267	237	237
q15	q16	819	773	710	710
q17	952	981	991	981
q18	6885	5782	5723	5723
q19	1160	1243	1186	1186
q20	513	421	263	263
q21	6093	2891	2626	2626
q22	441	375	313	313
Total cold run time: 104755 ms
Total hot run time: 29465 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	4845	4692	5048	4692
q2	q3	4911	5211	4663	4663
q4	2156	2216	1376	1376
q5	4941	4667	4726	4667
q6	227	177	135	135
q7	1852	1801	1557	1557
q8	2399	2030	1939	1939
q9	7471	7467	7391	7391
q10	4737	4690	4261	4261
q11	545	391	370	370
q12	734	732	532	532
q13	3095	3371	2813	2813
q14	282	269	253	253
q15	q16	687	704	606	606
q17	1294	1261	1279	1261
q18	7564	6990	6934	6934
q19	1154	1116	1090	1090
q20	2232	2236	1952	1952
q21	5311	4575	4461	4461
q22	532	470	417	417
Total cold run time: 56969 ms
Total hot run time: 51370 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 168328 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 4006aee3a4cc7b9112c427bbf83d5e175d9d4dab, data reload: false

query5	4372	630	478	478
query6	528	205	171	171
query7	4800	560	306	306
query8	388	213	197	197
query9	8776	4085	4072	4072
query10	482	312	251	251
query11	5928	2357	2186	2186
query12	159	97	96	96
query13	1337	621	436	436
query14	6377	5391	5056	5056
query14_1	4336	4420	4424	4420
query15	205	197	174	174
query16	1123	483	437	437
query17	1136	697	566	566
query18	2589	478	339	339
query19	216	180	147	147
query20	110	109	106	106
query21	219	151	118	118
query22	13655	13565	13383	13383
query23	17412	16573	16161	16161
query23_1	16319	16240	16344	16240
query24	7517	1800	1320	1320
query24_1	1322	1303	1308	1303
query25	615	471	423	423
query26	1398	329	163	163
query27	2597	536	338	338
query28	4471	2038	2031	2031
query29	1126	662	502	502
query30	340	249	203	203
query31	1121	1085	947	947
query32	106	61	63	61
query33	553	320	261	261
query34	1220	1154	669	669
query35	764	800	685	685
query36	1391	1417	1254	1254
query37	154	103	94	94
query38	3246	3155	3045	3045
query39	943	917	889	889
query39_1	878	875	889	875
query40	223	125	105	105
query41	75	67	66	66
query42	98	98	95	95
query43	330	333	285	285
query44	
query45	201	188	181	181
query46	1112	1157	734	734
query47	2319	2438	2263	2263
query48	396	438	312	312
query49	645	482	373	373
query50	995	348	261	261
query51	4315	4272	4235	4235
query52	89	92	83	83
query53	247	273	185	185
query54	297	230	206	206
query55	82	84	77	77
query56	269	232	236	232
query57	1405	1395	1312	1312
query58	252	229	240	229
query59	1599	1700	1419	1419
query60	300	271	246	246
query61	188	144	152	144
query62	692	648	583	583
query63	237	195	191	191
query64	2607	755	621	621
query65	
query66	1822	458	348	348
query67	30079	29734	28946	28946
query68	
query69	457	316	265	265
query70	982	951	956	951
query71	297	219	214	214
query72	2994	2694	2360	2360
query73	895	755	449	449
query74	5149	4942	4755	4755
query75	2651	2581	2212	2212
query76	2340	1151	779	779
query77	361	375	300	300
query78	12560	12462	11912	11912
query79	1541	1048	752	752
query80	741	468	377	377
query81	479	282	235	235
query82	574	156	126	126
query83	336	280	247	247
query84	
query85	922	508	424	424
query86	439	293	290	290
query87	3436	3315	3148	3148
query88	3702	2754	2791	2754
query89	442	380	330	330
query90	1819	184	179	179
query91	173	156	132	132
query92	65	60	56	56
query93	1505	1502	924	924
query94	658	356	295	295
query95	734	451	357	357
query96	1040	837	337	337
query97	2697	2710	2560	2560
query98	217	205	207	205
query99	1150	1172	1028	1028
Total cold run time: 252971 ms
Total hot run time: 168328 ms

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.

3 participants