Thread
Commits
GET /api/v1/messages/:b64id/commits
the thread's linked commits as JSON, with link sources.
API reference →
-
Speed up eqjoinsel() with lots of MCV entries.
- 057012b205a0 19 (unreleased) cited
-
[PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Chengpeng Yan <chengpeng_yan@outlook.com> — 2026-01-21T14:13:56Z
Hi, `compute_distinct_stats()` is used for data types that have an equality operator but no ordering (so we can't use the sort-based path). It keeps a `track[]` array of candidate MCVs and, for each sampled row, searches that array for a match. With high `statistics_target`, that becomes O(n) per sample and can dominate ANALYZE time. This patch keeps the existing behavior but adds a fast lookup path: - When `attstattarget >= 100`, and the type's default hash support matches the equality operator, build a small `simplehash` table mapping a tracked Datum to its current `track[]` slot (average O(1) match lookup). - Otherwise, fall back to the existing linear scan. While here, the singleton-eviction logic changes from repeatedly shifting the count=1 region to a simple round-robin cursor over that region. This keeps replacement O(1) and (when hashing is enabled) avoids having to update many hash->index mappings. Performance I used a small harness focusing on this code path (xid and aclitem, with match-heavy / unique-heavy / Zipf-ish distributions). Setup: - MacBook Pro (Apple M4 Max, 36GB RAM), macOS 26.1 (Darwin 25.1.0) - Reported numbers are median of 5 runs - For the "after" numbers below, I set the hash threshold to 0 to show the potential benefit; the patch as attached enables hashing only for `attstattarget >= 100`. Results (ms; before -> after): obj | statistics_target | before_ms | after_ms | speedup_x ----+------------------+----------+---------+--------- bench_aclitem.x_match | 25 | 174.154 | 182.346 | 0.96 bench_aclitem.x_match | 50 | 55.708 | 55.693 | 1.00 bench_aclitem.x_match | 100 | 74.311 | 63.978 | 1.16 bench_aclitem.x_match | 200 | 143.621 | 86.790 | 1.65 bench_aclitem.x_match | 500 | 462.616 | 125.083 | 3.70 bench_aclitem.x_match | 1000 | 1590.918 | 202.849 | 7.84 bench_aclitem.x_match | 2000 | 5857.253 | 406.840 | 14.40 bench_aclitem.x_match | 5000 | 30844.177 | 1678.826 | 18.37 bench_aclitem.x_match | 10000 | 73134.141 | 9207.071 | 7.94 bench_aclitem.x_unique | 25 | 18.214 | 17.962 | 1.01 bench_aclitem.x_unique | 50 | 39.305 | 37.045 | 1.06 bench_aclitem.x_unique | 100 | 76.555 | 64.800 | 1.18 bench_aclitem.x_unique | 200 | 151.416 | 90.619 | 1.67 bench_aclitem.x_unique | 500 | 497.243 | 134.351 | 3.70 bench_aclitem.x_unique | 1000 | 1788.755 | 209.299 | 8.55 bench_aclitem.x_unique | 2000 | 7249.638 | 332.865 | 21.78 bench_aclitem.x_unique | 5000 | 50785.046 | 604.944 | 83.95 bench_aclitem.x_unique | 10000 | 195506.022 | 792.658 | 246.65 bench_aclitem.x_zipf | 25 | 17.451 | 19.770 | 0.88 bench_aclitem.x_zipf | 50 | 37.131 | 35.560 | 1.04 bench_aclitem.x_zipf | 100 | 76.053 | 67.494 | 1.13 bench_aclitem.x_zipf | 200 | 145.435 | 100.484 | 1.45 bench_aclitem.x_zipf | 500 | 429.574 | 131.647 | 3.26 bench_aclitem.x_zipf | 1000 | 1313.187 | 223.918 | 5.86 bench_aclitem.x_zipf | 2000 | 4117.637 | 445.521 | 9.24 bench_aclitem.x_zipf | 5000 | 14863.325 | 1189.886 | 12.49 bench_aclitem.x_zipf | 10000 | 31706.434 | 2269.572 | 13.97 bench_xid.x_match | 25 | 22.136 | 56.767 | 0.39 bench_xid.x_match | 50 | 42.182 | 40.947 | 1.03 bench_xid.x_match | 100 | 81.089 | 67.848 | 1.20 bench_xid.x_match | 200 | 118.315 | 80.993 | 1.46 bench_xid.x_match | 500 | 403.674 | 120.421 | 3.35 bench_xid.x_match | 1000 | 1246.068 | 171.385 | 7.27 bench_xid.x_match | 2000 | 4374.122 | 406.823 | 10.75 bench_xid.x_match | 5000 | 21961.532 | 1903.715 | 11.54 bench_xid.x_match | 10000 | 55453.808 | 11035.402 | 5.03 bench_xid.x_unique | 25 | 21.062 | 20.011 | 1.05 bench_xid.x_unique | 50 | 40.473 | 41.414 | 0.98 bench_xid.x_unique | 100 | 79.711 | 67.382 | 1.18 bench_xid.x_unique | 200 | 125.255 | 72.853 | 1.72 bench_xid.x_unique | 500 | 412.332 | 96.030 | 4.29 bench_xid.x_unique | 1000 | 1373.882 | 144.837 | 9.49 bench_xid.x_unique | 2000 | 5084.898 | 277.216 | 18.34 bench_xid.x_unique | 5000 | 31412.454 | 574.693 | 54.66 bench_xid.x_unique | 10000 | 126639.495 | 804.947 | 157.33 bench_xid.x_zipf | 25 | 21.915 | 20.758 | 1.06 bench_xid.x_zipf | 50 | 41.458 | 39.498 | 1.05 bench_xid.x_zipf | 100 | 78.128 | 70.466 | 1.11 bench_xid.x_zipf | 200 | 118.361 | 79.816 | 1.48 bench_xid.x_zipf | 500 | 351.440 | 114.009 | 3.08 bench_xid.x_zipf | 1000 | 1049.475 | 173.981 | 6.03 bench_xid.x_zipf | 2000 | 3162.409 | 404.565 | 7.82 bench_xid.x_zipf | 5000 | 11170.524 | 1346.067 | 8.30 bench_xid.x_zipf | 10000 | 23928.836 | 2549.381 | 9.39 The bench harness (bench/) is attached for reproduction. Patch attached. -- Best regards, Chengpeng Yan
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> — 2026-01-21T20:44:08Z
Hi Chengpeng, On 21.01.2026 17:13, Chengpeng Yan wrote: > `compute_distinct_stats()` is used for data types that have an equality > operator but no ordering (so we can't use the sort-based path). It > keeps a `track[]` array of candidate MCVs and, for each sampled row, > searches that array for a match. With high `statistics_target`, that > becomes O(n) per sample and can dominate ANALYZE time. Nice catch - this is indeed not first time we run into an O(N^2) bottleneck in MCV array and address it with hash-based lookup. Thanks for working on this! I have a couple of follow-up questions after a quick look at the patch: 1. The hash table is created but I do not see a corresponding destroy call. 2. In the original code, when a value was not found using the nested-loop search, the singleton (count = 1) region was shifted to make room for the new entry. After the patch, I no longer see this shifting logic. I might be missing something, but it looks like the non-hash path now follows the same replacement behavior as the hash-based one. I’ll need a bit more time for a deeper review and some local benchmarks, but overall the approach looks interesting. -- Best regards, Ilia Evdokimov, Tantor Labs LLC, https://tantorlabs.com/
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Chengpeng Yan <chengpeng_yan@outlook.com> — 2026-01-22T09:40:38Z
Hi > On Jan 22, 2026, at 04:44, Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> wrote: > > Nice catch - this is indeed not first time we run into an O(N^2) bottleneck in MCV array and address it with hash-based lookup. Thanks for working on this! Thanks for the review. This change was actually inspired by your earlier patch on a similar issue, so thanks a lot for that as well. > I have a couple of follow-up questions after a quick look at the patch: > 1. The hash table is created but I do not see a corresponding destroy call. The hash table is allocated in the per-column temporary memory context (the “Analyze Column” context that is active while compute_stats() runs). That context is MemoryContextReset() after each column and eventually deleted at the end of ANALYZE, so the table is reclaimed automatically. This matches the existing convention noted at the end of compute_distinct_stats() (“We don’t need to bother cleaning up any of our temporary palloc’s”). That said, I can add an explicit DistinctHash_destroy() before returning for clarity, although it shouldn’t be required for correctness or leak prevention. > 2. In the original code, when a value was not found using the nested-loop search, the singleton (count = 1) region was shifted to make room for the new entry. After the patch, I no longer see this shifting logic. I might be missing something, but it looks like the non-hash path now follows the same replacement behavior as the hash-based one. In the original code, inserting a new distinct value into the singleton (count = 1) region effectively evicted the oldest singleton (FIFO) by inserting at the head of that region and shifting the tail, which is O(n) per new distinct value. The patch replaces this with a FIFO eviction cursor (effectively a circular / round-robin cursor) over the count = 1 region, avoiding repeated shifts. When hashing is enabled, this also avoids having to update many hash→index mappings due to element shifts. I kept the non-hash path using the same mechanism so that the hash and fallback paths follow the same replacement behavior, with the intention of preserving the original FIFO semantics while avoiding the O(n) cost. I’ve also posted a v2 update. It adjusts the eviction cursor when a count = 1 value is promoted (via the existing bubble-up logic), so that the cursor-based eviction is now exactly equivalent to the original shift-based FIFO behavior. I’ve also updated the relevant comments to clarify this behavior. The v2 patch is attached. -- Best regards, Chengpeng Yan
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> — 2026-01-29T21:54:15Z
On 22.01.2026 12:40, Chengpeng Yan wrote: > The v2 patch is attached. I took a deeper look at the v2 patch. The hash-based lookup itself looks correct, and when testing ANALYZE runtime with large default_statistics_target values, the patch indeed provides a noticeable speedup. I have one small suggestion regarding the handling of firstcount1 and c1_cursor in the 'match' path. When we find a match in track[], we increment count and perform bubble-up swaps to keep the array ordered by frequency. If the value previously has count = 1, it is effectively leaving the singleton (count = 1) region and becoming part of the count>1. Conceptually, this means that the boundary between these two regions (firstcount1) should move left by one. Given that, it seems sufficient to update the boundary and then ensure that c1_cursor still point inside the singleton region: if (was_count1 && j < firstcount1) firstcount1--; if (c1_cursor < firstcount1) c1_cursor = firstcount1; This avoids reasoning about specific shifted subranges (firstcount1..match_index). FIFO behavior is still preserved because c1_cursor is only advanced when an eviction actually happens. Let me know if I'm overlooking any corner cases. -- Best regards. Ilia Evdokimov, Tantor Labs LLC, https://tantorlabs.com/
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Tatsuya Kawata <kawatatatsuya0913@gmail.com> — 2026-02-01T09:09:43Z
Hi, As a newcomer who is still learning the codebase, I attempted to review the v2 patch. The behavior looks correct to me. I considered some edge cases and they all seem to be handled properly: - Edge case 1: c1_cursor is behind match_index This can occur when firstcount1 advances past c1_cursor due to bubble-up. It appears to be handled by the condition at L2318. - Edge case 2: All singletons are promoted, leaving no singletons to evict The condition `else if (firstcount1 < track_cnt)` at L2348 ensures that when firstcount1 == track_cnt (empty singleton region), the new value is simply skipped. This looks correct. - Edge case 3: c1_cursor exceeds track array bounds While Ilia's proposal would simplify this, it's currently handled by L2321. Regarding Ilia's simplification proposal: > if (was_count1 && j < firstcount1) > firstcount1--; > if (c1_cursor < firstcount1) > c1_cursor = firstcount1; The simplification looks appealing. However, I may be misunderstanding something - should the logic perhaps be: if (was_count1 && j <= firstcount1) firstcount1++; if (c1_cursor < firstcount1) c1_cursor = firstcount1; My reasoning: - Without `<=`, the case where j == firstcount1 would leave firstcount1 pointing to a value with count=2 (no longer a singleton). - I believe `firstcount1--` should be `firstcount1++`, since when a singleton is promoted to multiply-seen, the singleton region shrinks from the left, meaning the boundary moves right (increases). Please correct me if I'm missing something. Regards, Tatsuya Kawata -
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Chengpeng Yan <chengpeng_yan@outlook.com> — 2026-02-02T04:05:36Z
> On Feb 1, 2026, at 17:09, Tatsuya Kawata <kawatatatsuya0913@gmail.com> wrote: > > Regarding Ilia's simplification proposal: > > > if (was_count1 && j < firstcount1) > > firstcount1--; > > if (c1_cursor < firstcount1) > > c1_cursor = firstcount1; > > The simplification looks appealing. However, I may be misunderstanding something - > should the logic perhaps be: > > if (was_count1 && j <= firstcount1) > firstcount1++; > if (c1_cursor < firstcount1) > c1_cursor = firstcount1; > > My reasoning: > - Without `<=`, the case where j == firstcount1 would leave firstcount1 pointing > to a value with count=2 (no longer a singleton). > - I believe `firstcount1--` should be `firstcount1++`, since when a singleton > is promoted to multiply-seen, the singleton region shrinks from the left, > meaning the boundary moves right (increases). > > Please correct me if I'm missing something. > > Regards, > Tatsuya Kawata > Hi, Thanks for the careful review and the suggestions. For v3 I did not change the algorithm/behavior; I only adjusted the comment in the match path to make the bubble-up / cursor interaction more explicit. On the firstcount1 / c1_cursor handling: c1_cursor is an index-based “clock hand” over the count==1 region used to get FIFO eviction without physically shifting the singleton subarray. The extra cursor adjustment in the match path is there to preserve the same FIFO victim sequence as the original shift-based code. The key case is when a singleton is matched again (was_count1) and then bubbles up into the count>1 prefix. That bubble-up effectively shifts track[firstcount1..match_index-1] right by one. If c1_cursor points anywhere in that shifted range (or at match_index itself), it must advance by one as well; otherwise the cursor would start referring to a different element than before the shift and we’d evict a newer singleton next, breaking FIFO equivalence. Concrete example: singleton region is [A,B,C,D] and c1_cursor points to B (next to evict). If D is seen again, it becomes count=2 and bubbles up, shifting [A,B,C] one slot to the right. B’s index changes, so c1_cursor must move with it; otherwise the next eviction would incorrectly target A (or whatever now sits at the old index), not B as in the shift-based implementation. This is also why the condition uses <= match_index: the cursor may point at the promoted singleton slot as well. Regarding the boundary direction: since firstcount1 is “index of first singleton”, promotions shrink the singleton region and move the boundary to the right. In hash mode that’s handled by advancing firstcount1 while track[firstcount1].count > 1, and then clamping c1_cursor back into the singleton region if needed. If anything above still sounds off, happy to walk through a specific trace. -- Best regards, Chengpeng Yan
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Tatsuya Kawata <kawatatatsuya0913@gmail.com> — 2026-02-02T13:09:01Z
Hi, Thank you for the detailed explanation! Your explanation helped me understand the design much better. I hope my understanding is now on the right track. I tested v3 both approaches: 1. Ilia's proposal with corrected increment and <= condition: if (was_count1 && j <= firstcount1) firstcount1++; 2. The original patch with while loop: while (use_hash && firstcount1 < track_cnt && track[firstcount1].count > 1) firstcount1++; I verified the following cases and both approaches produced correct track array values after the loop completed: Case 1: c1_cursor == match_index c1_cursor points to a singleton, that singleton is matched again, bubble-up occurs, then a new value arrives triggering eviction. Case 2: c1_cursor < match_index c1_cursor is in the earlier part of the singleton region, and a singleton further back is matched. Case 3: c1_cursor > match_index c1_cursor has advanced past match_index due to previous evictions, and an earlier singleton is matched. Both approaches seem to work correctly. The code reduction from 1 is minimal, so either approach should be fine. I believe the while loop exists to handle potential edge cases, though in typical scenarios firstcount1 would only increment once per match (since one singleton is promoted at a time). Overall, the patch looks good to me. Regards, Tatsuya Kawata -
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Chengpeng Yan <chengpeng_yan@outlook.com> — 2026-02-03T04:33:35Z
> On Feb 2, 2026, at 21:09, Tatsuya Kawata <kawatatatsuya0913@gmail.com> wrote: > > Hi, > > Thank you for the detailed explanation! Your explanation helped me understand the design much better. > I hope my understanding is now on the right track. > > I tested v3 both approaches: > > 1. Ilia's proposal with corrected increment and <= condition: > if (was_count1 && j <= firstcount1) > firstcount1++; > > 2. The original patch with while loop: > while (use_hash && firstcount1 < track_cnt && > track[firstcount1].count > 1) > firstcount1++; > > I verified the following cases and both approaches produced correct > track array values after the loop completed: > > Case 1: c1_cursor == match_index > c1_cursor points to a singleton, that singleton is matched again, > bubble-up occurs, then a new value arrives triggering eviction. > > Case 2: c1_cursor < match_index > c1_cursor is in the earlier part of the singleton region, > and a singleton further back is matched. > > Case 3: c1_cursor > match_index > c1_cursor has advanced past match_index due to previous evictions, > and an earlier singleton is matched. > > Both approaches seem to work correctly. The code reduction from 1 is minimal, so either approach should be fine. > I believe the while loop exists to handle potential edge cases, > though in typical scenarios firstcount1 would only increment once per match (since one singleton is promoted at a time). > > Overall, the patch looks good to me. Hi Tatsuya, Thank you for the detailed testing and for validating those c1_cursor/match_index cases. I agree with your conclusion that both variants behave correctly, and that the code reduction from the single-step approach is small. On firstcount1: in the typical case it should advance by one when a singleton is promoted. I kept the loop-style adjustment mainly as an invariant repair step in hash mode — after bubble-up, it simply advances firstcount1 until it again points to the first singleton (or track_cnt). That makes the update less dependent on subtle index relationships and is a bit more robust against potential corner cases (or future tweaks to the reordering), while still being cheap since firstcount1 only moves forward and is bounded by track_cnt/track_max. That said, if other community members would prefer the simpler one-step update for readability, I’m happy to switch. -- Best regards, Chengpeng Yan
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
John Naylor <johncnaylorls@gmail.com> — 2026-02-03T05:39:48Z
On Tue, Feb 3, 2026 at 11:33 AM Chengpeng Yan <chengpeng_yan@outlook.com> wrote: > That said, if other community members would prefer the simpler one-step > update for readability, I’m happy to switch. To evaluate that, it would be easier if the patch were split into two commits, one with the simple step approach, and one that changes to the other approach. Also, we have some places that switch from e.g. an array to a hash table once a collection gets above a certain threshold. In this case, however, having less than 100 stat target is unheard of, in my experience. It's only a tiny amount of code, but it doesn't seem worth special-casing. Others may have a different opinion, and I'm not sure we don't already have this behavior elsewhere. -- John Naylor Amazon Web Services
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Chengpeng Yan <chengpeng_yan@outlook.com> — 2026-02-07T13:39:08Z
> On Feb 3, 2026, at 13:39, John Naylor <johncnaylorls@gmail.com> wrote: > > To evaluate that, it would be easier if the patch were split into two > commits, one with the simple step approach, and one that changes to > the other approach. > > Also, we have some places that switch from e.g. an array to a hash > table once a collection gets above a certain threshold. In this case, > however, having less than 100 stat target is unheard of, in my > experience. It's only a tiny amount of code, but it doesn't seem worth > special-casing. Others may have a different opinion, and I'm not sure > we don't already have this behavior elsewhere. Thanks for the suggestion — I’ve applied it in v4 by splitting the changes into two commits. 0001 contains the main optimization and uses the simpler one-step update for firstcount1 in the hash path. 0002 is a small follow-up that switches that logic to the more invariant-preserving form discussed earlier. Regarding the hash threshold: my initial approach followed existing postgres precedent for hash-based MCV matching. For example, commit 057012b205a (“Speed up eqjoinsel() with lots of MCV entries”) introduces EQJOINSEL_MCV_HASH_THRESHOLD in selfuncs.c, only enabling hashing once the MCV set is large enough to amortize the setup cost. The exact threshold differs, but the pattern is similar. Also, while 100 is the default statistics target, there are internal cases that use smaller values — e.g. vacuumdb staged analyze temporarily sets default_statistics_target to 1 and 10 in vacuuming.c — which was part of the motivation for keeping the thresholded behavior. That said, I’m happy to adjust or remove the threshold if the consensus is to simplify this further. -- Best regards, Chengpeng Yan
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> — 2026-02-16T16:16:26Z
On 07.02.2026 16:39, Chengpeng Yan wrote: > > > On Feb 3, 2026, at 13:39, John Naylor <johncnaylorls@gmail.com> wrote: > > > > To evaluate that, it would be easier if the patch were split into two > > commits, one with the simple step approach, and one that changes to > > the other approach. > > > > Also, we have some places that switch from e.g. an array to a hash > > table once a collection gets above a certain threshold. In this case, > > however, having less than 100 stat target is unheard of, in my > > experience. It's only a tiny amount of code, but it doesn't seem worth > > special-casing. Others may have a different opinion, and I'm not sure > > we don't already have this behavior elsewhere. > > Thanks for the suggestion — I’ve applied it in v4 by splitting the > changes into two commits. I think it would be beneficial to split it into two logically independent parts. Right now the patch introduces two different changes at once: 1. Replacing the original shift-based singleton handling with the cursor based eviction mechanism. 2. Adding the hash-based lookup. Splitting the patch would make review easier because I can first focus purely on the algorithmic transformation (shift -> cursor) and verify semantic equivalence. > > Regarding the hash threshold: my initial approach followed existing > postgres precedent for hash-based MCV matching. For example, commit > 057012b205a (“Speed up eqjoinsel() with lots of MCV entries”) introduces > EQJOINSEL_MCV_HASH_THRESHOLD in selfuncs.c, only enabling hashing once > the MCV set is large enough to amortize the setup cost. The exact > threshold differs, but the pattern is similar. > > Also, while 100 is the default statistics target, there are internal > cases that use smaller values — e.g. vacuumdb staged analyze temporarily > sets default_statistics_target to 1 and 10 in vacuuming.c — which was > part of the motivation for keeping the thresholded behavior. > > That said, I’m happy to adjust or remove the threshold if the consensus > is to simplify this further. I would probably not rely too heavily on EQJOINSEL_MCV_HASH_THRESHOLD as a direct reference point here. That threshold is somewhat heuristic as well, and the cost trade-offs in eqjoinsel() are not identical to what we have in compute_distinct_stats(). In particular, here the break-even point will strongly depend on the data type and value width. For example, with wider datums (e.g. bytea), the comparison cost increases, and the threshold at which hashing amortizes its setup cost may shift noticeably. We don't need to compute the exact optimal threshold - just get a reasonable order. -- Best regards. Ilia Evdokimov, Tantor Labs LLC, https://tantorlabs.com/
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> — 2026-02-26T11:46:32Z
Have you benchmarked this change except in first message in this thread? While reviewing the patch more closely, I noticed that compute_distinct_stats() is only used for types where we have =, != but not <. In practice, most common scalar types go through compute_scalar_stats() instead. That makes me wonder how often this optimization would actually trigger in real workloads. Since compute_scalar_stats() is the more common path, there's chance that the hash-table based improvement in compute_distinct_stats() may not provide a noticeable overall benefit. -- Best regards, Ilia Evdokimov, Tantor Labs LLC, https://tantorlabs.com/
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Chengpeng Yan <chengpeng_yan@outlook.com> — 2026-04-14T01:34:02Z
Hi Ilia, Thanks for the review. > On Feb 17, 2026, at 00:16, Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> wrote: > > I think it would be beneficial to split it into two logically independent parts. Right now the patch introduces two different changes at once: > 1. Replacing the original shift-based singleton handling with the cursor based eviction mechanism. > 2. Adding the hash-based lookup. > Splitting the patch would make review easier because I can first focus purely on the algorithmic transformation (shift -> cursor) and verify semantic equivalence. I split v5 accordingly. The first patch changes the singleton handling from shifting to a cursor-based eviction scheme, and the second patch adds the hash lookup. > On Feb 26, 2026, at 19:46, Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> wrote: > > Have you benchmarked this change except in first message in this thread? Yes. I reran the same bench scripts from the first post, unchanged. For this rerun I kept the same comparison setup as in that first message: one run on the pre-change path with no hash lookup, and one run with hash lookup forced on for all statistics targets by setting the threshold to 0. Across the full bench run, the hash-enabled variant shows a 3.963x geomean speedup. For statistics_target >= 100, all measured points improved, with a 5.578x geomean speedup in that region. A few representative rows are: - bench_aclitem.x_unique, target 10000: 204740.298 ms -> 941.056 ms (217.564x) - bench_xid.x_unique, target 10000: 138083.532 ms -> 984.214 ms (140.298x) At lower targets, the effect is smaller and mixed. For example: - bench_xid.x_match, target 50: 42.245 ms -> 43.690 ms (0.967x) > I would probably not rely too heavily on EQJOINSEL_MCV_HASH_THRESHOLD as a direct reference point here. That threshold is somewhat heuristic as well, and the cost trade-offs in eqjoinsel() are not identical to what we have in compute_distinct_stats(). In particular, here the break-even point will strongly depend on the data type and value width. For example, with wider datums (e.g. bytea), the comparison cost increases, and the threshold at which hashing amortizes its setup cost may shift noticeably. We don't need to compute the exact optimal threshold - just get a reasonable order. I agree that EQJOINSEL_MCV_HASH_THRESHOLD is not a strong direct reference point here, and I am not treating 100 as something to inherit from eqjoinsel(). I reran the bench mainly to sanity-check the threshold for compute_distinct_stats() itself. In this setup, forcing hashing at all statistics targets gives mixed results below 100, while from 100 upward the effect is consistently positive. So in the attached v5 I kept 100 as a conservative heuristic for this relatively narrow path, rather than as a claim that it is an exact or universally optimal cutoff. That seemed enough to establish a reasonable order for the threshold, even if it is not meant as an exact break-even point for every type. > While reviewing the patch more closely, I noticed that compute_distinct_stats() is only used for types where we have =, != but not <. In practice, most common scalar types go through compute_scalar_stats() instead. > > That makes me wonder how often this optimization would actually trigger in real workloads. Since compute_scalar_stats() is the more common path, there's chance that the hash-table based improvement in compute_distinct_stats() may not provide a noticeable overall benefit. I agree that this is a relatively narrow optimization. In core, the main direct built-in cases on this path are xid, cid, and aclitem, plus some derived cases over equality-only types. So the scope here is limited, but when compute_distinct_stats() is exercised, especially with higher statistics targets, the improvement is substantial. I am attaching v5 as a two-patch series, together with bench-compare-result.out containing the full no-hash vs all-hash comparison. -- Best regards, Chengpeng Yan
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> — 2026-05-03T14:16:24Z
On 4/14/26 04:34, Chengpeng Yan wrote: > I split v5 accordingly. The first patch changes the singleton handling > from shifting to a cursor-based eviction scheme, and the second patch > adds the hash lookup. I reviewed v5 of the patches. Instead of going through each issue one by one, I made a pass to clean up and clarify the code and summarize the main changes belows: - Fixed a few typos in comments and added comments in places where the logic was not immediately clear; - Rewrote the bubble-up loop using `for` loop, which I find more readable. Also removed some confusing uses of the `j` variable that mase the flow harder to follow; - Simplified parts of the code to improve overall readability. -- Best regards, Ilia Evdokimov, Tantor Labs LLC, https://tantorlabs.com/
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
John Naylor <johncnaylorls@gmail.com> — 2026-05-04T02:10:16Z
On Thu, Feb 26, 2026 at 6:46 PM Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> wrote: > While reviewing the patch more closely, I noticed > that compute_distinct_stats() is only used for types where we have =, != > but not <. In practice, most common scalar types go through > compute_scalar_stats() instead. > > That makes me wonder how often this optimization would actually trigger > in real workloads. Since compute_scalar_stats() is the more common path, > there's chance that the hash-table based improvement in > compute_distinct_stats() may not provide a noticeable overall benefit. Coming back to this point, in any installation, the common path is going to vastly outnumber the rare path, so this patch is optimizing the wrong thing. -- John Naylor Amazon Web Services
-
Re: [PATCH] ANALYZE: hash-accelerate MCV tracking for equality-only types
Chengpeng Yan <chengpeng_yan@outlook.com> — 2026-05-06T04:51:45Z
Hi, > On May 3, 2026, at 22:16, Ilia Evdokimov <ilya.evdokimov@tantorlabs.com> wrote: > > I reviewed v5 of the patches. Instead of going through each issue one by one, I made a pass to clean up and clarify the code and summarize the main changes belows: > - Fixed a few typos in comments and added comments in places where the logic was not immediately clear; > - Rewrote the bubble-up loop using `for` loop, which I find more readable. Also removed some confusing uses of the `j` variable that mase the flow harder to follow; > - Simplified parts of the code to improve overall readability. Thanks for the review and for working on this. After giving it more thought, I agree with John’s point that the set of types benefiting from this optimization is currently too limited. While this optimization can still provide noticeable improvements in some specific cases, those cases do not seem common enough at this stage, so the overall benefit also seems fairly small. Because of that, I marked this patch as Withdrawn in the current CommitFest and plan to pause further work on it for now. That said, if postgres or some extensions introduce more commonly used types that are a good fit for this optimization, I think it would make sense to revisit and potentially revive this work. Thanks again for the effort and contribution. -- Best regards, Chengpeng Yan