Skip to content

executor: Improve HashAgg string collation key caching - #10978

Merged
ti-chi-bot[bot] merged 5 commits into
pingcap:masterfrom
ChangRui-Ryan:changrui_ci_cache
Aug 4, 2026
Merged

executor: Improve HashAgg string collation key caching#10978
ti-chi-bot[bot] merged 5 commits into
pingcap:masterfrom
ChangRui-Ryan:changrui_ci_cache

Conversation

@ChangRui-Ryan

@ChangRui-Ryan ChangRui-Ryan commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #10979

Problem Summary

For string GROUP BY keys using a case-insensitive collation such as utf8mb4_general_ci, HashAgg needs to generate a collation sort key before probing or inserting into the hash table.

Previously, the sort key was generated for every input row. When the grouped column has a low number of distinct raw values, the same raw string is converted to the same sort key repeatedly, causing unnecessary collation processing and memory writes.

For example:

SELECT l_shipmode, COUNT(*)
FROM lineitem
GROUP BY l_shipmode;

l_shipmode usually has a very low NDV. If it uses utf8mb4_general_ci, repeatedly generating sort keys for millions of rows can consume a significant portion of the HashAgg execution time.

At the same time, unconditionally caching sort keys is not suitable for high-NDV inputs because maintaining the cache may introduce additional hash lookups and memory usage without enough cache hits. Therefore, the optimization needs to provide substantial benefits for low-NDV workloads while keeping the high-NDV overhead small and bounded.

What is changed and how it works

This PR adds an always-on collation sort-key cache to the batch string-key processing path of HashAgg.

For each input Block, the cache maps a raw string value to its generated collation sort key:

raw string -> collation sort key

When processing a row:

  1. Look up the raw string in the cache.
  2. If it is found, reuse the cached sort key.
  3. If it is not found, generate the sort key once and insert it into the cache.
  4. Use the resulting sort key for the existing hash-table lookup or insertion.

The cache is enabled only when all of the following conditions are satisfied:

  • HashAgg uses the batch string-key processing path.
  • The string key uses a supported case-insensitive collation.
  • The current processing range contains at least 1,024 rows.

To bound the cost for high-NDV inputs, the maximum number of cached distinct values is calculated from the number of rows in the current Block:

cache distinct limit = rows in the Block / 32

For a typical maximum runtime Block containing 65,536 rows, the cache can hold at most 2,048 distinct raw values.

If a cache miss occurs after the distinct-value limit has been reached, the current Block falls back to direct sort-key generation. The fallback state is also recorded in the worker's AggregatedDataVariants, so subsequent Blocks processed by the same aggregation worker do not attempt to initialize the cache again. This worker-level once-fallback behavior prevents high-NDV workloads from repeatedly paying the cache construction and lookup overhead for every Block.

The cache itself remains local to one Block. It only stores references to raw strings owned by that Block, so no input-column memory needs to be retained across Blocks. Only the fallback decision is preserved across Blocks.

The optimization does not change collation or grouping semantics. Hash-table keys are still the sort keys generated by the existing collator; the change only avoids regenerating an identical sort key for repeated raw string values.

The benchmark uses 128 Blocks with 65,536 rows per Block:

Workload Baseline With cache Change
NDV 64 27.16 M rows/s 73.41 M rows/s +170.3%
NDV 2,048 26.05 M rows/s 55.71 M rows/s +113.9%
NDV 2,049, fallback 26.95 M rows/s 26.85 M rows/s -0.4%
NDV 65,536, fallback 24.17 M rows/s 23.99 M rows/s -0.7%
First 64 Blocks low NDV, remaining Blocks high NDV 25.40 M rows/s 35.75 M rows/s +40.8%

The results show substantial improvements for low-NDV and mixed workloads, while the worker-level fallback keeps the high-NDV overhead below 1% in the benchmark.


Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No code

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

None

Summary by CodeRabbit

  • Performance

    • Improved aggregation performance for case-insensitive string data by reusing collation sort keys during batch processing.
    • Automatically switches to uncached processing when distinct values exceed the cache threshold, helping maintain predictable performance.
  • Bug Fixes

    • Improved consistency when processing subsequent batches after cache fallback.
  • Tests

    • Added coverage for collation-sensitive grouping, varying distinct-value counts, and fallback behavior.
    • Added benchmarks for cached and uncached aggregation scenarios.

@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-linked-issue release-note-none Denotes a PR that doesn't merit a release note. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e09ddb88-7ac2-4375-a142-3b9a356b8d3a

📥 Commits

Reviewing files that changed from the base of the PR and between 6321c17 and 261be19.

📒 Files selected for processing (1)
  • dbms/src/Common/ColumnsHashing.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • dbms/src/Common/ColumnsHashing.h

📝 Walkthrough

Walkthrough

The PR adds CI collation sort-key caching to string hash aggregation, tracks persistent fallback after cache thresholds are exceeded, wires initialization and status propagation through Aggregator, and adds executor tests plus NDV-focused benchmarks.

Changes

Collation sort-key caching

Layer / File(s) Summary
Cache-enabled string hashing
dbms/src/Common/ColumnsHashing.h, dbms/src/Common/ColumnsHashingImpl.h
String batching caches CI collation sort keys until the distinct-key limit, then switches to direct computation through new base and string-hash APIs.
Aggregation cache lifecycle
dbms/src/Interpreters/Aggregator.h, dbms/src/Interpreters/Aggregator.cpp
Aggregation state records fallback, initializes caching for eligible batches, and propagates fallback status across later blocks.
Behavior validation and performance coverage
dbms/src/Flash/tests/gtest_aggregation_executor.cpp, dbms/src/Flash/tests/bench_aggregation_hash_map.cpp
Tests cover CI equivalence, collation key sizing, NDV behavior, and worker fallback. Benchmarks cover low, threshold, high, and transitioning NDV workloads.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Aggregator
  participant HashMethodString
  participant Collator
  participant AggregatedDataVariants
  Aggregator->>AggregatedDataVariants: check fallback state
  Aggregator->>HashMethodString: initialize cache for batch
  HashMethodString->>Collator: generate or reuse sort keys
  HashMethodString-->>Aggregator: return fallback status
  Aggregator->>AggregatedDataVariants: persist fallback status
Loading

Poem

I’m a rabbit with keys in a row,
Reusing the sort signs I know.
When distinct paths grow wide,
I switch cache aside—
And hop through the blocks as they flow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the HashAgg string collation key caching improvement.
Description check ✅ Passed The description explains the problem, implementation, tests, performance results, side effects, and release-note status.
Linked Issues check ✅ Passed The changes directly implement issue #10979 by caching repeated collation sort keys in HashAgg string grouping.
Out of Scope Changes check ✅ Passed The implementation, tests, and benchmarks are related to the linked issue and stated HashAgg caching objectives.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
dbms/src/Common/ColumnsHashing.h (1)

102-141: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reserve the lookup hash map here too.

cached_sort_keys is already reserved for the expected distinct count, but raw_to_sort_key_index still grows from its default capacity and will rehash repeatedly as the cache fills. Reserving it alongside the vector avoids avoidable rehashes.

♻️ Proposed fix
         collation_sort_key_cache_active = true;
         collation_sort_key_cache_distinct_limit = rows / max_collation_sort_key_cache_distinct_ratio;
         cached_sort_keys.reserve(collation_sort_key_cache_distinct_limit + 1);
+        raw_to_sort_key_index.reserve(collation_sort_key_cache_distinct_limit + 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dbms/src/Common/ColumnsHashing.h` around lines 102 - 141, Reserve
raw_to_sort_key_index for collation_sort_key_cache_distinct_limit when
initializing the sort-key cache, alongside the existing cached_sort_keys
reservation, so filling the cache does not trigger repeated hash-map rehashes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@dbms/src/Common/ColumnsHashing.h`:
- Around line 102-141: Reserve raw_to_sort_key_index for
collation_sort_key_cache_distinct_limit when initializing the sort-key cache,
alongside the existing cached_sort_keys reservation, so filling the cache does
not trigger repeated hash-map rehashes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 06cf7ea1-eedb-4167-958d-12fb9dbf3da4

📥 Commits

Reviewing files that changed from the base of the PR and between b5093dd and 8eb6ffc.

📒 Files selected for processing (6)
  • dbms/src/Common/ColumnsHashing.h
  • dbms/src/Common/ColumnsHashingImpl.h
  • dbms/src/Flash/tests/bench_aggregation_hash_map.cpp
  • dbms/src/Flash/tests/gtest_aggregation_executor.cpp
  • dbms/src/Interpreters/Aggregator.cpp
  • dbms/src/Interpreters/Aggregator.h

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

3 similar comments
@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

1 similar comment
@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

Copilot AI 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.

Pull request overview

This PR improves HashAgg performance for case-insensitive string GROUP BY keys by avoiding repeated collation sort-key generation when raw string values repeat frequently. It introduces a bounded, block-scoped cache in the batch string-key processing path and records a worker-level “once fallback” flag to avoid repeated cache overhead on high-NDV inputs.

Changes:

  • Add a collation sort-key cache for batch string-key hashing when using CI collations, with a distinct-key limit to bound overhead.
  • Persist a worker-level fallback flag (AggregatedDataVariants) to skip cache attempts after a high-NDV fallback is detected.
  • Add unit tests and a micro-benchmark covering semantics, cache sizing behavior, and fallback behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
dbms/src/Interpreters/Aggregator.h Adds worker-level flag to remember collation sort-key cache fallback.
dbms/src/Interpreters/Aggregator.cpp Initializes cache per execution range and propagates fallback to the worker variant.
dbms/src/Common/ColumnsHashingImpl.h Adds no-op cache hooks to the generic hash method base.
dbms/src/Common/ColumnsHashing.h Implements the CI-collation sort-key cache in the batch string-key handler.
dbms/src/Flash/tests/gtest_aggregation_executor.cpp Adds unit tests validating semantics and worker-level once-fallback behavior.
dbms/src/Flash/tests/bench_aggregation_hash_map.cpp Adds benchmarks for low/high NDV and NDV transition workloads for the new cache.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

agg_process_info.resetBlock(*block);
do
{
aggregator->executeOnBlock(agg_process_info, *data_variants, 1);
@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

2 similar comments
@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/test pull-sanitizer-asan

1 similar comment
@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/test pull-sanitizer-asan

@windtalker windtalker 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.

Looks like this cache only works for cases that group by a single string column, it does not work for something like group by int_col, string_col?

Comment thread dbms/src/Common/ColumnsHashing.h Outdated
const StringRef & raw_key,
std::string & sort_key_container)
{
if (!collation_sort_key_cache_active)

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.

add unlikely hint for this if?

auto * aggregates_pool = result.aggregates_pool;
typename Method::State state(agg_process_info.key_columns, key_sizes, collators);
if (!result.string_collation_key_cache_fallback)
state.initCollationSortKeyCache(agg_process_info.end_row - agg_process_info.start_row);

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.

agg_process_info.end_row - agg_process_info.start_row is not always represent a whole block, which means the cache is not block level cache, is it expected?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. In the normal aggregation path, start_row is 0 and end_row is block.rows(), so the cache is initialized with the whole Block size and is reused across all mini-batches processed by the same executeImplInner() invocation.

The range can be smaller than a whole Block when aggregation is interrupted by a partial-processing path, such as spilling after a ResizeException. In that case, a subsequent executeOnBlock() invocation processes the remaining rows and creates a new local cache for that range.

This is expected. The cache is intentionally scoped to the current processing range rather than guaranteed to span the entire input Block. The cache only affects sort-key computation and does not affect correctness. The worker-level fallback state is kept separately in AggregatedDataVariants::string_collation_key_cache_fallback, so once fallback happens, later Blocks still skip cache attempts.

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

Looks like this cache only works for cases that group by a single string column, it does not work for something like group by int_col, string_col?

Yes, this cache currently only applies to HashMethodString, which is used for grouping by a single string column. Composite keys such as (int_col, string_col) use HashMethodSerialized and are not covered by this optimization. We prefer to keep this change scoped.

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

@windtalker windtalker 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

@ti-chi-bot ti-chi-bot Bot added the approved label Aug 3, 2026
@ti-chi-bot ti-chi-bot Bot added the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Aug 3, 2026
@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

2 similar comments
@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

@ChangRui-Ryan

Copy link
Copy Markdown
Contributor Author

/retest

@ti-chi-bot

ti-chi-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: windtalker, xzhangxian1008

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added lgtm and removed needs-1-more-lgtm Indicates a PR needs 1 more LGTM. labels Aug 4, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

[LGTM Timeline notifier]

Timeline:

  • 2026-08-03 09:00:31.208396267 +0000 UTC m=+2432217.244491333: ☑️ agreed by windtalker.
  • 2026-08-04 03:45:09.181307097 +0000 UTC m=+2499695.217402143: ☑️ agreed by xzhangxian1008.

@ti-chi-bot
ti-chi-bot Bot merged commit 7340ae9 into pingcap:master Aug 4, 2026
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved lgtm release-note-none Denotes a PR that doesn't merit a release note. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimize HashAgg by caching collation sort keys for repeated string keys

4 participants