Skip to content

[improvement](fe) Estimate wide metadata-cache values one element at a time - #69558

Merged
morningman merged 1 commit into
apache:masterfrom
morningman:b2m-t12e-wide-value-estimation
Oct 25, 2026
Merged

morningman merged 1 commit into
apache:masterfrom
morningman:b2m-t12e-wide-value-estimation

Conversation

@morningman

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: #66717, #67726

Problem Summary:

In short. With a metadata-cache weight limit set, the cached metadata of a wide table (thousands of columns), a long name list or a long partition list was never cached: its size estimate gave up and the value was reloaded on every statement. This PR weighs such values one element at a time, so they are admitted and charged for their size, while the safety bound of the estimate stays in place.

Background. Master (#67726) lets the metadata caches of external catalogs be bounded by weight: the FE-global external_meta_cache_max_weight, a catalog's meta.cache.max-weight, or an entry's meta.cache.<engine>.<entry>.max-weight. Once any of them applies, each value is weighed before it is admitted. Several entries use the complete walk of ReflectiveObjectSizeEstimator, which visits every object of the value and gives up after 10,000 of them, so that one pathological value cannot cost unbounded CPU and temporary memory. An estimate that gives up is incomplete, and an incomplete value is returned to the query but not cached. branch-4.x (#66717) governs only a few entries and weighs them with per-element formulas, so it has no such limit.

The problem, and what it cost. The 10,000-visit budget is reached by values that are large only because of one list, long before they are large in bytes. Measured on master, the first value that never enters a weighted cache:

Entry Value Never cached from
hms-table Hive table 995 INT / 382 struct<a:int,b:string> columns
paimon-table, paimon-schema-at Paimon schema 2,497 INT / 908 ROW<a INT, b STRING> columns
fe-core schema (default, doris engines) schema of any other external catalog 3,332 INT / 832 struct columns
adbc-table-schema Arrow schema 2,499 INT / 1,110 struct fields
adbc-table-names, adbc-namespaces name list 9,999 names
max-compute-partition partition list 1,249 three-level partitions

For such a table every statement goes back to the source (an HMS call, a Paimon schema file read, an ODPS partition listing, an ADBC metadata call), and weight_reject_count in information_schema.catalog_meta_cache_statistics keeps growing. In a unit test with a 128 MiB global limit, three reads of a 2,000-column HMS table loaded it three times and cached nothing.

How this PR fixes it.

  • ReflectiveObjectSizeEstimator.estimateCompletePerElement(root, collections...) walks root like the complete estimate, except that each listed collection it reaches is counted as its own storage plus every element estimated separately, each with its own 10,000-visit budget. An element that appears in more than one of the listed collections is counted once. MetaCacheSizeEstimators.reflectivePerElement(...) and reflectiveEachElement() wrap it as cache estimators.
  • The affected entries use it: the HMS table (columns, partition keys), the Paimon table schema and the schema-at snapshot (fields), the fe-core schema cache (SchemaCacheValue.getColumnLists(): schema, and for plugin tables also partition and display columns), the ADBC table schema (fields) and name lists, and the MaxCompute partition list.
  • The visit budget still bounds every element and the rest of the value, so a value that reaches a huge shared structure still fails fast, and the temporary identity set holds one element's graph at a time instead of the whole value's. The whole-value walk and its budget are unchanged for every other entry.
  • Objects that elements share (for example a scalar type singleton) are counted once per element, so the weight is somewhat above the retained size (measured 1.1x to 1.6x) and never below it. A plugin table's display columns reuse its column objects and are not counted twice.

Results.

Value, under a weight limit Before After
3,000-column HMS table (INT or struct columns) estimate incomplete, reloaded on every read complete, loaded once, weight_reject_count = 0
4,000-column Paimon table, INT or ROW columns; 4,000-field schema-at snapshot incomplete complete, loaded once
fe-core schema with 4,000 INT + 1,000 struct columns incomplete complete, loaded once
3,000-field ADBC schema; 20,000 table names incomplete complete, loaded once
10,000 MaxCompute partitions incomplete complete, loaded once
single element larger than the visit budget incomplete incomplete (unchanged)

The new tests also check that the estimate is never below an unbounded walk of the value and at most twice it, and that it doubles when the column count doubles. With the per-element walk disabled, all nine new wide-value tests fail; without the element de-duplication, a plugin schema with reused display columns weighs twice as much (2,052,208 to 4,104,168 bytes) and its test fails.

Release note

Fix: with a metadata-cache weight limit set, the schema of a table with thousands of columns (HMS, Paimon, ADBC and other external catalogs), a long ADBC name list and a long MaxCompute partition list are cached instead of being loaded again by every statement.

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

    New and changed tests: ReflectiveObjectSizeEstimatorTest (4 new), HmsCacheSizeEstimatorTest (new, 2), PaimonCacheSizeEstimatorTest, PaimonSchemaAtMemoTest, AdbcMetadataCacheTest, MaxComputePartitionCacheTest (1 new each), ExternalCatalogMetaCacheTest (2 new). All tests of fe-connector-cache (164), hms (163), paimon (847), adbc (191), maxcompute (157), hive (510), hudi (270) and iceberg (1689) pass, as do 14 fe-core metadata and schema cache test classes (204 tests). No regression test: a weight limit and a table with thousands of columns are needed to see the difference, and the unit tests drive the real cache admission path.

  • Behavior changed:

    • No.
    • Yes.
      Wide values that a weighted metadata cache used to reject are admitted and weighed as the sum of their elements.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

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

…a time

### What problem does this PR solve?

Issue Number: None

Related PR: apache#66717, apache#67726

Problem Summary: Once any metadata-cache weight limit is set (the FE-global
external_meta_cache_max_weight, a catalog meta.cache.max-weight or an entry
max-weight), every cached value is weighed before it is admitted. Master
(apache#67726) weighs several entries with the complete walk of
ReflectiveObjectSizeEstimator over the whole value, which gives up after
10,000 visited objects. A value that is large only because of one long list
reaches that long before it is large in bytes, and an incomplete estimate is
rejected: the value is returned to the query but never cached, so every
statement loads it again (an HMS call, a Paimon schema file read, an ODPS
partition listing) and weight_reject_count keeps growing. Measured, the
first value that never enters a weighted cache:

    hms-table                       995 INT / 382 struct<a:int,b:string> columns
    paimon-table, paimon-schema-at  2,497 INT / 908 ROW columns
    fe-core schema cache            3,332 INT / 832 struct columns
    adbc-table-schema               2,499 INT / 1,110 struct fields
    adbc-table-names, namespaces    9,999 names
    max-compute-partition           1,249 three-level partitions

branch-4.x weighs its few governed entries with per-element formulas and
has no such limit.

The complete estimate can now walk the long lists of a value element by
element: ReflectiveObjectSizeEstimator.estimateCompletePerElement counts a
listed collection's own storage and estimates each element with its own
visit budget, counting an element that appears in several of the lists
once. The HMS table (columns and partition keys), the Paimon table schema
and schema-at snapshot (fields), the fe-core schema cache (schema, partition
and display columns), the ADBC table schema (fields) and name lists, and the
MaxCompute partition list use it. The visit budget still bounds every
element and the rest of the value, so a value that reaches a huge shared
structure still fails fast, and the temporary identity set holds one
element's graph at a time. Objects that elements share, such as a scalar
type singleton, are counted once per element: the weight can be about 1.1x
to 1.6x of the retained size, never below it. The whole-value walk and its
10,000-visit budget are unchanged for every other entry.

### Release note

Fix: with a metadata-cache weight limit set, the schema of a table with
thousands of columns (HMS, Paimon, ADBC and other external catalogs), a long
ADBC name list and a long MaxCompute partition list are cached instead of
being loaded again by every statement.

### Check List (For Author)

- Test: Unit Test
    - ReflectiveObjectSizeEstimatorTest, HmsCacheSizeEstimatorTest (new), PaimonCacheSizeEstimatorTest, PaimonSchemaAtMemoTest, AdbcMetadataCacheTest, MaxComputePartitionCacheTest, ExternalCatalogMetaCacheTest
    - fe-connector-cache / hms / paimon / adbc / maxcompute / hive / hudi / iceberg module tests, 14 fe-core metadata and schema cache test classes
- Behavior changed: Yes. Wide values that a weighted metadata cache used to reject are admitted, weighed as the sum of their elements.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@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?

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor 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.

Reviewed #69558 at 65757d9. No verified new findings and no existing blocking inline findings. The review converged after two rounds, including a final check of this frozen review payload.

All 17 changed Java files and related load, admission, refresh, invalidation and schema-construction paths were reviewed statically. No builds or tests were run, and no repository source was modified. There was no additional user-provided focus.

Critical checkpoint conclusions:

  • Goal and evidence: selected long metadata lists are weighed per element, allowing wide values to pass complete weighted admission. Added tests exercise real admission/repeated reads, growth, shared-column de-duplication and oversized-element rejection; they were assessed statically.
  • Scope and reuse: the shared estimator and wrappers centralize the change; connector integrations and the engine schema accessor are focused. No unrelated source changes were found.
  • Concurrency and locks: Walkers and identity sets are invocation-local. Existing ClassValue plans, SQL-thread loads, refresh-executor work, scope/key generation fences and reservation replacement remain in place. Miss estimation is outside publication locks; replacement paths retain their existing guarded sections. No new lock ordering or concrete deadlock was identified.
  • Lifecycle and initialization: temporary graph state is released with each estimate; entry reservations still release through existing discard/invalidation/close paths. Paimon retains its existing allowance for later store growth. No new static initialization dependency was introduced.
  • Configuration: no settings were added. Existing global/catalog/entry weight limits still control admission; unweighted caches avoid the estimation path.
  • Compatibility: no shared connector SPI, persisted format or FE/BE protocol was changed; the new helpers and engine methods are additive.
  • Thrift/protobuf: no definition, field ID or enum changes.
  • Parallel paths: HMS columns/partition keys, Paimon file-store/wrapper tables and schema-at snapshots, ADBC namespaces/names/Arrow fields, MaxCompute partitions, and default/Doris/plugin engine schemas were checked. Copied display columns are charged and reused column identities are de-duplicated. Untouched Paimon non-FileStoreTable row-type estimates retain their existing whole-graph limit.
  • Special conditions and errors: collection selection uses retained object identity. Every selected element keeps the depth and visit limits; element or residual-graph failures produce incomplete estimates and reject weighted admission while returning the loaded metadata.
  • Tests and negative cases: new unit coverage includes wide primitive/nested schemas, long names/partitions and shared display roots; existing depth, inaccessible-field and budget failure cases remain. The publication machinery is unchanged. Runtime behavior and test execution were not verified in this static-only review.
  • Test result artifacts: no regression output files or generated expected results changed.
  • Observability: existing weight-reject counts/reasons and cache diagnostics cover this path; no new distributed operation requires additional tracing.
  • Transactions and persistence: no journal, replay, failover or transaction-state changes; committed-data visibility, visible versions and delete-bitmap semantics are unaffected by these estimator changes.
  • Data writes and atomicity: no data mutation path changed; existing cache publication and reservation fencing remains authoritative.
  • FE/BE propagation: no new cross-process variables or sending paths.
  • Performance and memory: work occurs at weighted admission rather than cache hits. The outer lists are traversed linearly with bounded object visits per element; shared descendant graphs may be revisited and conservatively overcounted. De-duplication retains O(number of selected elements) identities in addition to one bounded element graph, so total temporary memory is not constant in list length. No material failure was established from this tradeoff.
  • Other issues and focus: the final recheck found no additional substantiated issue. No user-specific focus or prior inline finding required a separate disposition.

Coverage limit: static source and test review only; no compilation, runtime reproduction or performance measurements were performed. No unresolved candidate or uncovered changed-file scope remains.

Review converged after 2 round(s).

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17593	5185	5083	5083
q2	2136	319	258	258
q3	10207	1369	838	838
q4	4679	462	336	336
q5	7427	799	545	545
q6	188	187	164	164
q7	752	787	604	604
q8	9249	1363	1114	1114
q9	5544	4264	4248	4248
q10	6830	1289	1036	1036
q11	432	256	231	231
q12	635	400	301	301
q13	18085	3713	3060	3060
q14	273	275	249	249
q15	q16	726	713	674	674
q17	1203	754	595	595
q18	7053	6185	6116	6116
q19	1193	948	611	611
q20	398	343	229	229
q21	5888	2556	2536	2536
q22	383	345	274	274
Total cold run time: 100874 ms
Total hot run time: 29102 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5978	5936	6182	5936
q2	745	557	563	557
q3	5542	5765	5252	5252
q4	2063	2187	1670	1670
q5	5717	5709	5710	5709
q6	237	187	152	152
q7	2237	1998	1766	1766
q8	3060	2687	2670	2670
q9	7908	7921	7905	7905
q10	3648	3574	3166	3166
q11	589	417	406	406
q12	673	682	521	521
q13	3328	3686	3010	3010
q14	286	306	267	267
q15	q16	662	678	618	618
q17	7642	7088	6995	6995
q18	12983	12180	12954	12180
q19	943	816	840	816
q20	2154	2140	1915	1915
q21	5852	5047	4892	4892
q22	514	462	402	402
Total cold run time: 72761 ms
Total hot run time: 66805 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150755 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 65757d98075286b3e078c0b083367294fcbfa16a, data reload: false

query5	4330	619	445	445
query6	453	206	193	193
query7	4794	436	239	239
query8	331	179	168	168
query9	8803	3116	3094	3094
query10	446	309	253	253
query11	5931	3867	3586	3586
query12	153	91	86	86
query13	1237	419	325	325
query14	6140	3944	3666	3666
query14_1	3447	3435	3442	3435
query15	208	199	180	180
query16	961	419	401	401
query17	863	653	545	545
query18	2419	439	314	314
query19	201	166	139	139
query20	85	84	85	84
query21	215	133	111	111
query22	12956	12911	12794	12794
query23	13569	12760	12384	12384
query23_1	12613	12375	12506	12375
query24	7370	826	475	475
query24_1	482	479	477	477
query25	545	426	367	367
query26	1270	263	144	144
query27	2750	443	276	276
query28	4614	1950	1954	1950
query29	1570	626	460	460
query30	304	221	185	185
query31	1028	940	825	825
query32	156	102	101	101
query33	541	313	255	255
query34	876	842	503	503
query35	758	759	667	667
query36	800	791	726	726
query37	134	103	85	85
query38	1815	1722	1697	1697
query39	801	813	746	746
query39_1	720	738	739	738
query40	217	117	100	100
query41	73	69	69	69
query42	88	85	83	83
query43	351	367	320	320
query44	1307	686	698	686
query45	186	181	173	173
query46	825	977	573	573
query47	2892	2984	2780	2780
query48	288	311	214	214
query49	570	400	293	293
query50	667	271	207	207
query51	10458	10316	10287	10287
query52	80	80	68	68
query53	186	203	154	154
query54	234	196	195	195
query55	73	78	63	63
query56	257	217	200	200
query57	1629	1595	1538	1538
query58	304	263	254	254
query59	2113	2137	1921	1921
query60	274	241	234	234
query61	147	145	137	137
query62	384	330	282	282
query63	188	162	164	162
query64	2772	968	762	762
query65	3336	3282	3280	3280
query66	1769	408	320	320
query67	20320	20247	19965	19965
query68	2911	997	618	618
query69	388	295	258	258
query70	881	815	797	797
query71	294	236	211	211
query72	2708	2608	2280	2280
query73	521	529	296	296
query74	4633	4500	4343	4343
query75	2358	2282	1927	1927
query76	2315	1006	604	604
query77	345	361	274	274
query78	9656	9745	9169	9169
query79	1004	865	523	523
query80	591	419	355	355
query81	573	318	265	265
query82	577	146	110	110
query83	282	195	182	182
query84	311	124	106	106
query85	812	415	378	378
query86	315	237	230	230
query87	2042	1949	1806	1806
query88	3699	2656	2642	2642
query89	328	280	243	243
query90	1873	175	174	174
query91	160	149	128	128
query92	99	90	90	90
query93	1001	1010	568	568
query94	449	316	272	272
query95	581	382	302	302
query96	675	491	233	233
query97	2477	2417	2343	2343
query98	165	151	154	151
query99	765	766	643	643
Total cold run time: 234248 ms
Total hot run time: 150755 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.26 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 65757d98075286b3e078c0b083367294fcbfa16a, data reload: false

query1	0.00	0.00	0.00
query2	0.09	0.05	0.05
query3	0.26	0.10	0.08
query4	1.61	0.10	0.10
query5	0.26	0.24	0.24
query6	1.16	0.68	0.67
query7	0.04	0.01	0.01
query8	0.05	0.04	0.03
query9	0.42	0.34	0.36
query10	0.57	0.57	0.58
query11	0.20	0.14	0.14
query12	0.18	0.15	0.15
query13	0.50	0.51	0.47
query14	1.02	1.02	1.01
query15	0.66	0.61	0.61
query16	0.36	0.35	0.35
query17	1.10	1.13	1.19
query18	0.21	0.19	0.19
query19	2.15	1.96	2.00
query20	0.02	0.02	0.01
query21	15.50	0.19	0.13
query22	5.04	0.05	0.06
query23	16.26	0.32	0.12
query24	3.17	0.51	0.42
query25	0.14	0.07	0.04
query26	1.22	0.22	0.17
query27	0.06	0.04	0.04
query28	3.89	0.80	0.38
query29	12.54	4.12	3.22
query30	0.29	0.13	0.09
query31	2.77	0.55	0.32
query32	3.22	0.61	0.51
query33	3.17	3.22	3.20
query34	15.48	3.95	3.36
query35	3.34	3.35	3.36
query36	0.55	0.44	0.41
query37	0.09	0.07	0.06
query38	0.05	0.04	0.03
query39	0.04	0.03	0.03
query40	0.17	0.15	0.14
query41	0.09	0.04	0.03
query42	0.04	0.02	0.03
query43	0.05	0.04	0.03
Total cold run time: 98.03 s
Total hot run time: 24.26 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 100.00% (6/6) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 50.00% (3/6) 🎉
Increment coverage report
Complete coverage report

@morningman
morningman merged commit be63e5f into apache:master Oct 25, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants