public inbox for [email protected]help / color / mirror / Atom feed
[PATCH 7/9] Remove the special batch mode, use a larger buffer always 71+ messages / 5 participants [nested] [flat]
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..08d0d55b06 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------0E72B707603BED22B4040825 Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210211.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 853 ++++++++++++-------- 1 file changed, 526 insertions(+), 327 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 69a72da337..fd85c18d83 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -57,11 +57,11 @@ #include "access/brin.h" #include "access/brin_internal.h" #include "access/brin_tuple.h" -#include "access/hash.h" /* XXX strange that it fails because of BRIN_AM_OID without this */ #include "access/reloptions.h" #include "access/stratnum.h" #include "access/htup_details.h" #include "catalog/pg_type.h" +#include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "utils/array.h" #include "utils/builtins.h" @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------22A4B241170149838D4D1F8F Content-Type: text/x-patch; charset=UTF-8; name="0008-Define-multi-minmax-oclasses-for-types-with-20210215.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0008-Define-multi-minmax-oclasses-for-types-with-20210215.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH 7/9] Remove the special batch mode, use a larger buffer always @ 2021-02-02 00:57 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw) Instead of using a batch mode (with a larger input buffer) only for new ranges, which introduces "special cases" in various places, use it as the standard approach. Also, instead of sizing the buffer to cover the whole range, limit it to some reasonable limit (10x the user-specified size). That should give us most of the benefits without consuming a lot of memory. --- src/backend/access/brin/brin_minmax_multi.c | 851 ++++++++++++-------- 1 file changed, 525 insertions(+), 326 deletions(-) diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index d1eafa9700..90d17e5008 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -92,7 +92,15 @@ */ #define PROCNUM_BASE 11 -#define MINMAX_LOAD_FACTOR 0.75 +/* + * Sizing the insert buffer - we use 10x the number of values specified + * in the reloption, but we cap it to 8192 not to get too large. When + * the buffer gets full, we reduce the number of values by half. + */ +#define MINMAX_BUFFER_FACTOR 10 +#define MINMAX_BUFFER_MIN 256 +#define MINMAX_BUFFER_MAX 8192 +#define MINMAX_BUFFER_LOAD_FACTOR 0.5 typedef struct MinmaxMultiOpaque { @@ -155,23 +163,24 @@ typedef struct Ranges Oid typid; Oid colloid; AttrNumber attno; + FmgrInfo *cmp; /* (2*nranges + nvalues) <= maxvalues */ int nranges; /* number of ranges in the array (stored) */ + int nsorted; /* number of sorted values (ranges + points) */ int nvalues; /* number of values in the data array (all) */ int maxvalues; /* maximum number of values (reloption) */ /* - * In batch mode, we simply add the values into a buffer, without any - * expensive steps (sorting, deduplication, ...). The buffer is sized - * to be larger than the target number of values per range, which - * reduces the number of compactions - operating on larger buffers is - * significantly more efficient, in most cases. We keep the actual - * target and compact to the requested number of values at the very - * end, before serializing to on-disk representation. + * We simply add the values into a large buffer, without any expensive + * steps (sorting, deduplication, ...). The buffer is a multiple of + * the target number of values, so the compaction happen less often, + * amortizing the costs. We keep the actual target and compact to + * the requested number of values at the very end, before serializing + * to on-disk representation. */ - bool batch_mode; - int target_maxvalues; /* requested number of values */ + /* requested number of values */ + int target_maxvalues; /* values stored for this range - either raw values, or ranges */ Datum values[FLEXIBLE_ARRAY_MEMBER]; @@ -203,7 +212,7 @@ typedef struct SerializedRanges static SerializedRanges *range_serialize(Ranges *range); -static Ranges *range_deserialize(SerializedRanges *range); +static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); /* Cache for support and strategy procesures. */ @@ -213,6 +222,14 @@ static FmgrInfo *minmax_multi_get_procinfo(BrinDesc *bdesc, uint16 attno, static FmgrInfo *minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, uint16 strategynum); +typedef struct compare_context +{ + FmgrInfo *cmpFn; + Oid colloid; +} compare_context; + +static int compare_values(const void *a, const void *b, void *arg); + /* * minmax_multi_init @@ -240,6 +257,57 @@ minmax_multi_init(int maxvalues) return ranges; } +static void +AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid); + + +static void +range_deduplicate_values(Ranges *range) +{ + int i, n; + int start; + compare_context cxt; + + /* + * If there are no unsorted values, we're done (this probably can't + * happen, as we're adding values to unsorted part). + */ + if (range->nsorted == range->nvalues) + return; + + /* sort the values */ + cxt.colloid = range->colloid; + cxt.cmpFn = range->cmp; + + /* how many values to sort? */ + start = 2 * range->nranges; + + qsort_arg(&range->values[start], + range->nvalues, sizeof(Datum), + compare_values, (void *) &cxt); + + n = 1; + for (i = 1; i < range->nvalues; i++) + { + /* same as preceding value, so store it */ + if (compare_values(&range->values[start + i - 1], + &range->values[start + i], + (void *) &cxt) == 0) + continue; + + range->values[start + n] = range->values[start + i]; + + n++; + } + + /* now all the values are sorted */ + range->nvalues = n; + range->nsorted = n; + + AssertCheckRanges(range, range->cmp, range->colloid); +} + + /* * range_serialize * Serialize the in-memory representation into a compact varlena value. @@ -262,14 +330,25 @@ range_serialize(Ranges *range) /* simple sanity checks */ Assert(range->nranges >= 0); + Assert(range->nsorted >= 0); Assert(range->nvalues >= 0); Assert(range->maxvalues > 0); + Assert(range->target_maxvalues > 0); + + /* at this point the range should be compacted to the target size */ + Assert(2*range->nranges + range->nvalues <= range->target_maxvalues); + + Assert(range->target_maxvalues <= range->maxvalues); + + /* range boundaries are always sorted */ + Assert(range->nvalues >= range->nsorted); + + /* sort and deduplicate values, if there's unsorted part */ + range_deduplicate_values(range); /* see how many Datum values we actually have */ nvalues = 2*range->nranges + range->nvalues; - Assert(2*range->nranges + range->nvalues <= range->maxvalues); - typid = range->typid; typbyval = get_typbyval(typid); typlen = get_typlen(typid); @@ -316,7 +395,7 @@ range_serialize(Ranges *range) serialized->typid = typid; serialized->nranges = range->nranges; serialized->nvalues = range->nvalues; - serialized->maxvalues = range->maxvalues; + serialized->maxvalues = range->target_maxvalues; /* * And now copy also the boundary values (like the length calculation @@ -367,7 +446,7 @@ range_serialize(Ranges *range) * in the in-memory value array. */ static Ranges * -range_deserialize(SerializedRanges *serialized) +range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -384,15 +463,18 @@ range_deserialize(SerializedRanges *serialized) nvalues = 2*serialized->nranges + serialized->nvalues; Assert(nvalues <= serialized->maxvalues); + Assert(serialized->maxvalues <= maxvalues); - range = minmax_multi_init(serialized->maxvalues); + range = minmax_multi_init(maxvalues); /* copy the header info */ range->nranges = serialized->nranges; range->nvalues = serialized->nvalues; - range->maxvalues = serialized->maxvalues; + range->nsorted = serialized->nvalues; + range->maxvalues = maxvalues; + range->target_maxvalues = serialized->maxvalues; + range->typid = serialized->typid; - range->batch_mode = false; typbyval = get_typbyval(serialized->typid); typlen = get_typlen(serialized->typid); @@ -439,12 +521,6 @@ range_deserialize(SerializedRanges *serialized) return range; } -typedef struct compare_context -{ - FmgrInfo *cmpFn; - Oid colloid; -} compare_context; - /* * Used to represent ranges expanded during merging and combining (to * reduce number of boundary values to store). @@ -528,6 +604,115 @@ compare_values(const void *a, const void *b, void *arg) return 0; } +void *bsearch_arg(const void *key, const void *base, + size_t nmemb, size_t size, + int (*compar) (const void *, const void *, void *), + void *arg); + +static bool +has_matching_range(BrinDesc *bdesc, Oid colloid, Ranges *ranges, + Datum newval, AttrNumber attno, Oid typid) +{ + Datum compar; + + Datum minvalue = ranges->values[0]; + Datum maxvalue = ranges->values[2*ranges->nranges - 1]; + + FmgrInfo *cmpLessFn; + FmgrInfo *cmpGreaterFn; + + /* binary search on ranges */ + int start, + end; + + if (ranges->nranges == 0) + return false; + + /* + * Otherwise, need to compare the new value with boundaries of all + * the ranges. First check if it's less than the absolute minimum, + * which is the first value in the array. + */ + cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTLessStrategyNumber); + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in the range list */ + if (DatumGetBool(compar)) + return false; + + /* + * And now compare it to the existing maximum (last value in the + * data array). But only if we haven't already ruled out a possible + * match in the minvalue check. + */ + cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, + BTGreaterStrategyNumber); + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + if (DatumGetBool(compar)) + return false; + + /* + * So we know it's in the general min/max, the question is whether it + * falls in one of the ranges or gaps. We'll use a binary search on + * the ranges. + * + * it's in the general range, but is it actually covered by any + * of the ranges? Repeat the check for each range. + * + * XXX We simply walk the ranges sequentially, but maybe we could + * further leverage the ordering and non-overlap and use bsearch to + * speed this up a bit. + */ + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) + { + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + return false; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); + + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); + continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); + + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); + continue; + } + + /* hey, we found a matching range */ + return true; + } + + return false; +} + + /* * range_contains_value * See if the new value is already contained in the range list. @@ -552,8 +737,6 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, Ranges *ranges, Datum newval) { int i; - FmgrInfo *cmpLessFn; - FmgrInfo *cmpGreaterFn; FmgrInfo *cmpEqualFn; Oid typid = attr->atttypid; @@ -562,77 +745,8 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, * range, and only when there's still a chance of getting a match we * inspect the individual ranges. */ - if (ranges->nranges > 0) - { - Datum compar; - bool match = true; - - Datum minvalue = ranges->values[0]; - Datum maxvalue = ranges->values[2*ranges->nranges - 1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - cmpLessFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTLessStrategyNumber); - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in the range list */ - if (DatumGetBool(compar)) - match = false; - - /* - * And now compare it to the existing maximum (last value in the - * data array). But only if we haven't already ruled out a possible - * match in the minvalue check. - */ - if (match) - { - cmpGreaterFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, - BTGreaterStrategyNumber); - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - if (DatumGetBool(compar)) - match = false; - } - - /* - * So it's in the general range, but is it actually covered by any - * of the ranges? Repeat the check for each range. - * - * XXX We simply walk the ranges sequentially, but maybe we could - * further leverage the ordering and non-overlap and use bsearch to - * speed this up a bit. - */ - for (i = 0; i < ranges->nranges && match; i++) - { - /* copy the min/max values from the ranges */ - minvalue = ranges->values[2*i]; - maxvalue = ranges->values[2*i+1]; - - /* - * Otherwise, need to compare the new value with boundaries of all - * the ranges. First check if it's less than the absolute minimum, - * which is the first value in the array. - */ - compar = FunctionCall2Coll(cmpLessFn, colloid, newval, minvalue); - - /* smaller than the smallest value in this range */ - if (DatumGetBool(compar)) - continue; - - compar = FunctionCall2Coll(cmpGreaterFn, colloid, newval, maxvalue); - - /* larger than the largest value in this range */ - if (DatumGetBool(compar)) - continue; - - /* hey, we found a matching row */ - return true; - } - } + if (has_matching_range(bdesc, colloid, ranges, newval, attno, typid)) + return true; cmpEqualFn = minmax_multi_get_strategy_procinfo(bdesc, attno, typid, BTEqualStrategyNumber); @@ -640,92 +754,42 @@ range_contains_value(BrinDesc *bdesc, Oid colloid, /* * We're done with the ranges, now let's inspect the exact values. * - * XXX Again, we do sequentially search the values - consider leveraging - * the ordering of values to improve performance. + * XXX We do sequential search for small number of values, and bsearch + * once we have more than 16 values. + * + * XXX We only inspect the sorted part - that's OK. For building it may + * produce false negatives, but only after we already added some values + * to the unsorted part, so we've modified the value. And when querying + * the index, there should be no unsorted values. */ - for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nvalues; i++) + if (ranges->nsorted >= 16) { - Datum compar; + compare_context cxt; - compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; - /* found an exact match */ - if (DatumGetBool(compar)) + if (bsearch_arg(&newval, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) != NULL) return true; } - - /* the value is not covered by this BRIN tuple */ - return false; -} - -/* - * insert_value - * Adds a new value into the single-point part, while maintaining ordering. - * - * The function inserts the new value to the right place in the single-point - * part of the range. It assumes there's enough free space, and then does - * essentially an insert-sort. - * - * XXX Assumes the 'values' array has space for (nvalues+1) entries, and that - * only the first nvalues are used. - */ -static void -insert_value(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues, - Datum newvalue) -{ - int i; - Datum lt; - - /* If there are no values yet, store the new one and we're done. */ - if (!nvalues) + else { - values[0] = newvalue; - return; - } - - /* - * A common case is that the new value is entirely out of the existing - * range, i.e. it's either smaller or larger than all previous values. - * So we check and handle this case first - first we check the larger - * case, because in that case we can just append the value to the end - * of the array and we're done. - */ + for (i = 2*ranges->nranges; i < 2*ranges->nranges + ranges->nsorted; i++) + { + Datum compar; - /* Is it greater than all existing values in the array? */ - lt = FunctionCall2Coll(cmp, colloid, values[nvalues-1], newvalue); - if (DatumGetBool(lt)) - { - /* just copy it in-place and we're done */ - values[nvalues] = newvalue; - return; - } + compar = FunctionCall2Coll(cmpEqualFn, colloid, newval, ranges->values[i]); - /* - * OK, I lied a bit - we won't check the smaller case explicitly, but - * we'll just compare the value to all existing values in the array. - * But we happen to start with the smallest value, so we're actually - * doing the check anyway. - * - * XXX We do walk the values sequentially. Perhaps we could/should be - * smarter and do some sort of bisection, to improve performance? - */ - for (i = 0; i < nvalues; i++) - { - lt = FunctionCall2Coll(cmp, colloid, newvalue, values[i]); - if (DatumGetBool(lt)) - { - /* - * Move values to make space for the new entry, which should go - * to index 'i'. Entries 0 ... (i-1) should stay where they are. - */ - memmove(&values[i+1], &values[i], (nvalues-i) * sizeof(Datum)); - values[i] = newvalue; - return; + /* found an exact match */ + if (DatumGetBool(compar)) + return true; } } - /* We should never really get here. */ - Assert(false); + /* the value is not covered by this BRIN tuple */ + return false; } #ifdef USE_ASSERT_CHECKING @@ -754,11 +818,12 @@ static void AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) { #ifdef USE_ASSERT_CHECKING - int i, j; + int i; /* some basic sanity checks */ Assert(ranges->nranges >= 0); - Assert(ranges->nvalues >= 0); + Assert(ranges->nsorted >= 0); + Assert(ranges->nvalues >= ranges->nsorted); Assert(ranges->maxvalues >= 2 * ranges->nranges + ranges->nvalues); Assert(ranges->typid != InvalidOid); @@ -770,32 +835,111 @@ AssertCheckRanges(Ranges *ranges, FmgrInfo *cmpFn, Oid colloid) */ AssertArrayOrder(cmpFn, colloid, ranges->values, 2*ranges->nranges); - /* finally check that none of the values are not covered by ranges */ + /* then the single-point ranges (with nvalues boundar values ) */ + AssertArrayOrder(cmpFn, colloid, &ranges->values[2*ranges->nranges], + ranges->nsorted); + + /* + * Check that none of the values are not covered by ranges (both + * sorted and unsorted) + */ for (i = 0; i < ranges->nvalues; i++) { + Datum compar; + int start, + end; + Datum minvalue, + maxvalue; + Datum value = ranges->values[2 * ranges->nranges + i]; - for (j = 0; j < ranges->nranges; j++) + if (ranges->nranges == 0) + break; + + minvalue = ranges->values[0]; + maxvalue = ranges->values[2*ranges->nranges - 1]; + + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); + + /* smaller than the smallest value in the first range */ + if (DatumGetBool(compar)) + continue; + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); + + /* larger than the largest value in the last range */ + if (DatumGetBool(compar)) + continue; + + start = 0; /* first range */ + end = ranges->nranges - 1; /* last range */ + while (true) { - Datum r; + int midpoint = (start + end) / 2; + + /* this means we ran out of ranges in the last step */ + if (start > end) + break; + + /* copy the min/max values from the ranges */ + minvalue = ranges->values[2 * midpoint]; + maxvalue = ranges->values[2 * midpoint + 1]; - Datum minval = ranges->values[2 * j]; - Datum maxval = ranges->values[2 * j + 1]; + /* + * Is the value smaller than the minval? If yes, we'll recurse + * to the left side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, value, minvalue); - /* if value is smaller than range minimum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, value, minval); - if (DatumGetBool(r)) + /* smaller than the smallest value in this range */ + if (DatumGetBool(compar)) + { + end = (midpoint - 1); continue; + } + + /* + * Is the value greater than the minval? If yes, we'll recurse + * to the right side of range array. + */ + compar = FunctionCall2Coll(cmpFn, colloid, maxvalue, value); - /* if value is greater than range maximum, that's OK */ - r = FunctionCall2Coll(cmpFn, colloid, maxval, value); - if (DatumGetBool(r)) + /* larger than the largest value in this range */ + if (DatumGetBool(compar)) + { + start = (midpoint + 1); continue; + } - /* value is between [min,max], which is wrong */ + /* hey, we found a matching range */ Assert(false); } } + + /* and values in the unsorted part must not be in sorted part */ + for (i = ranges->nsorted; i < ranges->nvalues; i++) + { + compare_context cxt; + Datum value = ranges->values[2 * ranges->nranges + i]; + + if (ranges->nsorted == 0) + break; + + cxt.colloid = ranges->colloid; + cxt.cmpFn = ranges->cmp; + + Assert(bsearch_arg(&value, &ranges->values[2*ranges->nranges], + ranges->nsorted, sizeof(Datum), + compare_values, (void *) &cxt) == NULL); + } #endif } @@ -1106,8 +1250,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, */ static CombineRange * build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, - bool addvalue, Datum newvalue, int *nranges, - bool deduplicate) + int *nranges) { int ncranges; CombineRange *cranges; @@ -1115,28 +1258,15 @@ build_combine_ranges(FmgrInfo *cmp, Oid colloid, Ranges *ranges, /* now do the actual merge sort */ ncranges = ranges->nranges + ranges->nvalues; - /* should we add an extra value? */ - if (addvalue) - ncranges += 1; - cranges = (CombineRange *) palloc0(ncranges * sizeof(CombineRange)); - /* put the new value at the beginning */ - if (addvalue) - { - cranges[0].minval = newvalue; - cranges[0].maxval = newvalue; - cranges[0].collapsed = true; - - /* then the regular and collapsed ranges */ - fill_combine_ranges(&cranges[1], ncranges-1, ranges); - } - else - fill_combine_ranges(cranges, ncranges, ranges); + /* fll the combine ranges */ + fill_combine_ranges(cranges, ncranges, ranges); /* and sort the ranges */ - ncranges = sort_combine_ranges(cmp, colloid, cranges, ncranges, - deduplicate); + ncranges = sort_combine_ranges(cmp, colloid, + cranges, ncranges, + true); /* deduplicate */ /* remember how many cranges we built */ *nranges = ncranges; @@ -1321,19 +1451,28 @@ store_combine_ranges(Ranges *ranges, CombineRange *cranges, int ncranges) } } + /* all the values are sorted */ + ranges->nsorted = ranges->nvalues; + Assert(count_values(cranges, ncranges) == 2*ranges->nranges + ranges->nvalues); Assert(2*ranges->nranges + ranges->nvalues <= ranges->maxvalues); } + + /* - * range_add_value - * Add the new value to the multi-minmax range. + * Consider freeing space in the ranges. + * + * Returns true if the value was actually modified. */ static bool -range_add_value(BrinDesc *bdesc, Oid colloid, - AttrNumber attno, Form_pg_attribute attr, - Ranges *ranges, Datum newval) +ensure_free_space_in_buffer(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *range) { + MemoryContext ctx; + MemoryContext oldctx; + FmgrInfo *cmpFn, *distanceFn; @@ -1342,109 +1481,44 @@ range_add_value(BrinDesc *bdesc, Oid colloid, int ncranges; DistanceValue *distances; - MemoryContext ctx; - MemoryContext oldctx; - - /* we'll certainly need the comparator, so just look it up now */ - cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, - BTLessStrategyNumber); - - /* comprehensive checks of the input ranges */ - AssertCheckRanges(ranges, cmpFn, colloid); - - Assert((ranges->nranges >= 0) && (ranges->nvalues >= 0) && (ranges->maxvalues >= 0)); - /* - * When batch-building, there should be no ranges. So either the - * number of ranges is 0 or we're not in batching mode. + * If there is free space in the buffer, we're done without having + * to modify anything. */ - Assert(!ranges->batch_mode || (ranges->nranges == 0)); - - /* When batch-building, just add it and we're done. */ - if (ranges->batch_mode) - { - /* there has to be free space, if we've sized the struct */ - Assert(ranges->nvalues < ranges->maxvalues); - - /* Make a copy of the value, if needed. */ - ranges->values[ranges->nvalues++] - = datumCopy(newval, attr->attbyval, attr->attlen);; - - return true; - } - - /* - * Bail out if the value already is covered by the range. - * - * We could also add values until we hit values_per_range, and then - * do the deduplication in a batch, hoping for better efficiency. But - * that would mean we actually modify the range every time, which means - * having to serialize the value, which does palloc, walks the values, - * copies them, etc. Not exactly cheap. - * - * So instead we do the check, which should be fairly cheap - assuming - * the comparator function is not very expensive. - * - * This also implies means the values array can't contain duplicities. - */ - if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + if (2*range->nranges + range->nvalues < range->maxvalues) return false; - /* Make a copy of the value, if needed. */ - newval = datumCopy(newval, attr->attbyval, attr->attlen); - - /* - * If there's space in the values array, copy it in and we're done. - * - * We do want to keep the values sorted (to speed up searches), so we - * do a simple insertion sort. We could do something more elaborate, - * e.g. by sorting the values only now and then, but for small counts - * (e.g. when maxvalues is 64) this should be fine. - */ - if (2*ranges->nranges + ranges->nvalues < ranges->maxvalues) - { - Datum *values; - - /* beginning of the 'single value' part (for convenience) */ - values = &ranges->values[2*ranges->nranges]; - - insert_value(cmpFn, colloid, values, ranges->nvalues, newval); - - ranges->nvalues++; - - /* - * Check we haven't broken the ordering of boundary values (checks - * both parts, but that doesn't hurt). - */ - AssertCheckRanges(ranges, cmpFn, colloid); + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); - /* Also check the range contains the value we just added. */ - // FIXME Assert(ranges, cmpFn, colloid); + /* Try deduplicating values in the unsorted part */ + range_deduplicate_values(range); - /* yep, we've modified the range */ + /* did we reduce enough free space by just the deduplication? */ + if (2*range->nranges + range->nvalues <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR) return true; - } /* - * Damn - the new value is not in the range yet, but we don't have space - * to just insert it. So we need to combine some of the existing ranges, - * to reduce the number of values we need to store (joining two intervals - * reduces the number of boundaries to store by 2). + * we need to combine some of the existing ranges, to reduce the number + * of values we need to store (joining intervals reduces the number of + * boundary values). * - * To do that we first construct an array of CombineRange items - each - * combine range tracks if it's a regular range or collapsed range, where - * "collapsed" means "single point." + * We first construct an array of CombineRange items - each combine range + * tracks if it's a regular range or a collapsed range, where "collapsed" + * means "single point." This makes the processing easier, as it allows + * handling ranges and points the same way. * - * Existing ranges (we have ranges->nranges of them) map to combine ranges - * directly, while single points (ranges->nvalues of them) have to be - * expanded. We neet the combine ranges to be sorted, and we do that by - * performing a merge sort of ranges, values and new value. + * Then we sort the combine ranges - this is necessary, because although + * ranges and points were sorted on their own, the new array is not. We + * do that by performing a merge sort of the two parts. * * The distanceFn calls (which may internally call e.g. numeric_le) may - * allocate quite a bit of memory, and we must not leak it. Otherwise - * we'd have problems e.g. when building indexes. So we create a local - * memory context and make sure we free the memory before leaving this - * function (not after every call). + * allocate quite a bit of memory, and we must not leak it (we might have + * to do this repeatedly, even for a single BRIN page range). Otherwise + * we'd have problems e.g. when building new indexes. So we use a memory + * context and make sure we free the memory at the end (so if we call + * the distance function many times, it might be an issue, but meh). */ ctx = AllocSetContextCreate(CurrentMemoryContext, "minmax-multi context", @@ -1453,9 +1527,7 @@ range_add_value(BrinDesc *bdesc, Oid colloid, oldctx = MemoryContextSwitchTo(ctx); /* OK build the combine ranges */ - cranges = build_combine_ranges(cmpFn, colloid, ranges, - true, newval, &ncranges, - false); + cranges = build_combine_ranges(cmpFn, colloid, range, &ncranges); /* and we'll also need the 'distance' procedure */ distanceFn = minmax_multi_get_procinfo(bdesc, attno, PROCNUM_DISTANCE); @@ -1469,21 +1541,104 @@ range_add_value(BrinDesc *bdesc, Oid colloid, * use too low or high value. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - ranges->maxvalues * MINMAX_LOAD_FACTOR, + range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR, cmpFn, colloid); - Assert(count_values(cranges, ncranges) <= ranges->maxvalues * MINMAX_LOAD_FACTOR); + Assert(count_values(cranges, ncranges) <= range->maxvalues * MINMAX_BUFFER_LOAD_FACTOR); /* decompose the combine ranges into regular ranges and single values */ - store_combine_ranges(ranges, cranges, ncranges); + store_combine_ranges(range, cranges, ncranges); MemoryContextSwitchTo(oldctx); MemoryContextDelete(ctx); /* Did we break the ranges somehow? */ + AssertCheckRanges(range, cmpFn, colloid); + + return true; +} + +/* + * range_add_value + * Add the new value to the multi-minmax range. + */ +static bool +range_add_value(BrinDesc *bdesc, Oid colloid, + AttrNumber attno, Form_pg_attribute attr, + Ranges *ranges, Datum newval) +{ + FmgrInfo *cmpFn; + bool modified = false; + + /* we'll certainly need the comparator, so just look it up now */ + cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); + + /* comprehensive checks of the input ranges */ AssertCheckRanges(ranges, cmpFn, colloid); + + /* + * Make sure there's enough free space in the buffer. We only trigger + * this when the buffer is full, which means it had to be modified as + * we size it to be larger than what is stored on disk. + * + * XXX This needs to happen before we check if the value is contained + * in the range, because the value might be in the unsorted part, and + * we don't check that in range_contains_value. The deduplication would + * then move it to the sorted part, and we'd add the value too, which + * violates the rule that we never have duplicates with the ranges + * or sorted values. + * + * XXX At the moment this only does the deduplication. + * + * XXX We might also deduplicate and recheck if the value is contained, + * but that seems like an overkill. We'd need to deduplicate anyway, + * so why not do it now. + */ + modified = ensure_free_space_in_buffer(bdesc, colloid, + attno, attr, ranges); + + /* + * Bail out if the value already is covered by the range. + * + * We could also add values until we hit values_per_range, and then + * do the deduplication in a batch, hoping for better efficiency. But + * that would mean we actually modify the range every time, which means + * having to serialize the value, which does palloc, walks the values, + * copies them, etc. Not exactly cheap. + * + * So instead we do the check, which should be fairly cheap - assuming + * the comparator function is not very expensive. + * + * This also implies means the values array can't contain duplicities. + */ + if (range_contains_value(bdesc, colloid, attno, attr, ranges, newval)) + return modified; + + /* Make a copy of the value, if needed. */ + newval = datumCopy(newval, attr->attbyval, attr->attlen); + + /* + * If there's space in the values array, copy it in and we're done. + * + * We do want to keep the values sorted (to speed up searches), so we + * do a simple insertion sort. We could do something more elaborate, + * e.g. by sorting the values only now and then, but for small counts + * (e.g. when maxvalues is 64) this should be fine. + */ + ranges->values[2*ranges->nranges + ranges->nvalues] = newval; + ranges->nvalues++; + + /* + * Check we haven't broken the ordering of boundary values (checks + * both parts, but that doesn't hurt). + */ + AssertCheckRanges(ranges, cmpFn, colloid); + + /* Also check the range contains the value we just added. */ // FIXME Assert(ranges, cmpFn, colloid); + /* yep, we've modified the range */ return true; } @@ -1506,12 +1661,6 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) MemoryContext ctx; MemoryContext oldctx; - /* - * This should only be used in batch mode, and there should be no - * ranges, just individual values. - */ - Assert((ranges->batch_mode) && (ranges->nranges == 0)); - /* we'll certainly need the comparator, so just look it up now */ cmpFn = minmax_multi_get_strategy_procinfo(bdesc, ranges->attno, ranges->typid, BTLessStrategyNumber); @@ -1534,8 +1683,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) /* OK build the combine ranges */ cranges = build_combine_ranges(cmpFn, ranges->colloid, ranges, - false, (Datum) 0, &ncranges, - true); /* deduplicate */ + &ncranges); /* deduplicate */ if (ncranges > 1) { @@ -1548,7 +1696,7 @@ compactify_ranges(BrinDesc *bdesc, Ranges *ranges, int max_values) * don't expect more tuples to be inserted soon. */ ncranges = reduce_combine_ranges(cranges, ncranges, distances, - max_values, cmpFn, ranges->colloid); + max_values, cmpFn, ranges->colloid); Assert(count_values(cranges, ncranges) <= max_values); } @@ -2052,8 +2200,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) * In batch mode, we need to compress the accumulated values to the * actually requested number of values/ranges. */ - if (ranges->batch_mode) - compactify_ranges(bdesc, ranges, ranges->target_maxvalues); + compactify_ranges(bdesc, ranges, ranges->target_maxvalues); s = range_serialize(ranges); dst[0] = PointerGetDatum(s); @@ -2114,15 +2261,39 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int target_maxvalues; + int maxvalues; BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + /* what was specified as a reloption? */ + target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(target_maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, target_maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + oldctx = MemoryContextSwitchTo(column->bv_context); - ranges = minmax_multi_init(MaxHeapTuplesPerPage * pagesPerRange); + ranges = minmax_multi_init(maxvalues); ranges->attno = attno; ranges->colloid = colloid; ranges->typid = attr->atttypid; - ranges->batch_mode = true; - ranges->target_maxvalues = brin_minmax_multi_get_values(bdesc, opts); + ranges->target_maxvalues = target_maxvalues; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); MemoryContextSwitchTo(oldctx); @@ -2136,10 +2307,38 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) { MemoryContext oldctx; + int maxvalues; + BlockNumber pagesPerRange = BrinGetPagesPerRange(bdesc->bd_index); + oldctx = MemoryContextSwitchTo(column->bv_context); serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + + /* + * Determine the insert buffer size - we use 10x the target, capped + * to the maximum number of values in the heap range. This is more + * than enough, considering the actual number of rows per page is + * likely much lower, but meh. + */ + maxvalues = Min(serialized->maxvalues * MINMAX_BUFFER_FACTOR, + MaxHeapTuplesPerPage * pagesPerRange); + + /* but always at least the original value */ + maxvalues = Max(maxvalues, serialized->maxvalues); + + /* always cap by MIN/MAX */ + maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); + maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); + + ranges = range_deserialize(maxvalues, serialized); + + ranges->attno = attno; + ranges->colloid = colloid; + ranges->typid = attr->atttypid; + + /* we'll certainly need the comparator, so just look it up now */ + ranges->cmp = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, + BTLessStrategyNumber); column->bv_mem_value = PointerGetDatum(ranges); column->bv_serialize = brin_minmax_multi_serialize; @@ -2184,7 +2383,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized); + ranges = range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2371,8 +2570,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a); - ranges_b = range_deserialize(serialized_b); + ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2408,7 +2607,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) cmpFn = minmax_multi_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - /* sort the combine ranges (don't deduplicate) */ + /* sort the combine ranges (no need to deduplicate) */ sort_combine_ranges(cmpFn, colloid, cranges, ncranges, false); /* @@ -2637,7 +2836,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges); + ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %u nvalues: %u maxvalues: %u", ranges_deserialized->nranges, -- 2.26.2 --------------4B194FF8F3EA3786FF9EAE1F Content-Type: text/x-patch; charset=UTF-8; name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210203.pa"; filename*1="tch" ^ permalink raw reply [nested|flat] 71+ messages in thread
* [PATCH] Teach pg_waldump to extract FPIs from the WAL @ 2022-04-22 22:51 David Christensen <[email protected]> 2022-04-23 14:49 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL Matthias van de Meent <[email protected]> 2022-04-25 11:03 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL Bharath Rupireddy <[email protected]> 0 siblings, 2 replies; 71+ messages in thread From: David Christensen @ 2022-04-22 22:51 UTC (permalink / raw) To: pgsql-hackers Hi -hackers, Enclosed is a patch to allow extraction/saving of FPI from the WAL stream via pg_waldump. Description from the commit: Extracts full-page images from the WAL stream into a target directory, which must be empty or not exist. These images are subject to the same filtering rules as normal display in pg_waldump, which means that you can isolate the full page writes to a target relation, among other things. Files are saved with the filename: <lsn>.<ts>.<db>.<rel>.<blk> with formatting to make things somewhat sortable; for instance: 00000000-010000C0.1663.1.6117.0 00000000-01000150.1664.0.6115.0 00000000-010001E0.1664.0.6114.0 00000000-01000270.1663.1.6116.0 00000000-01000300.1663.1.6113.0 00000000-01000390.1663.1.6112.0 00000000-01000420.1663.1.8903.0 00000000-010004B0.1663.1.8902.0 00000000-01000540.1663.1.6111.0 00000000-010005D0.1663.1.6110.0 It's noteworthy that the raw images do not have the current LSN stored with them in the WAL stream (as would be true for on-heap versions of the blocks), nor would the checksum be valid in them (though WAL itself has checksums, so there is some protection there). This patch chooses to place the LSN and calculate the proper checksum (if non-zero in the source image) in the outputted block. (This could perhaps be a targetted flag if we decide we don't always want this.) These images could be loaded/inspected via `pg_read_binary_file()` and used in the `pageinspect` suite of tools to perform detailed analysis on the pages in question, based on historical information, and may come in handy for forensics work. Best, David Attachments: [application/octet-stream] v1-pg_waldump-save-fpi.patch (10.3K, ../../CAOxo6XKjQb2bMSBRpePf3ZpzfNTwjQUc4Tafh21=jzjX6bX8CA@mail.gmail.com/2-v1-pg_waldump-save-fpi.patch) download | inline diff: From e9063dba6d6eac55db15c148380a85aa394f647b Mon Sep 17 00:00:00 2001 From: David Christensen <[email protected]> Date: Wed, 20 Apr 2022 19:59:35 -0500 Subject: [PATCH] Teach pg_waldump to extract FPIs from the WAL stream Extracts full-page images from the WAL stream into a target directory, which must be empty or not exist. These images are subject to the same filtering rules as normal display in pg_waldump, which means that you can isolate the full page writes to a target relation, among other things. Files are saved with the filename: <lsn>.<ts>.<db>.<rel>.<blk> with formatting to make things somewhat sortable; for instance: 00000000-010000C0.1663.1.6117.0 00000000-01000150.1664.0.6115.0 00000000-010001E0.1664.0.6114.0 00000000-01000270.1663.1.6116.0 00000000-01000300.1663.1.6113.0 00000000-01000390.1663.1.6112.0 00000000-01000420.1663.1.8903.0 00000000-010004B0.1663.1.8902.0 00000000-01000540.1663.1.6111.0 00000000-010005D0.1663.1.6110.0 It's noteworthy that the raw images do not have the current LSN stored with them in the WAL stream (as would be true for on-heap versions of the blocks), nor would the checksum be valid in them (though WAL itself has checksums, so there is some protection there). This patch chooses to place the LSN and calculate the proper checksum (if non-zero in the source image) in the outputted block. (This could perhaps be a targetted flag if we decide we don't always want this.) These images could be loaded/inspected via `pg_read_binary_file()` and used in the `pageinspect` suite of tools to perform detailed analysis on the pages in question, based on historical information, and may come in handy for forensics work. --- doc/src/sgml/ref/pg_waldump.sgml | 57 +++++++++++++ src/bin/pg_waldump/pg_waldump.c | 132 ++++++++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml index 57746d9421..215e63d962 100644 --- a/doc/src/sgml/ref/pg_waldump.sgml +++ b/doc/src/sgml/ref/pg_waldump.sgml @@ -240,6 +240,63 @@ PostgreSQL documentation </listitem> </varlistentry> + <varlistentry> + <term><option>-W <replaceable>save_path</replaceable></option></term> + <term><option>--raw-fpi=<replaceable>save_path</replaceable></option></term> + <listitem> + <para> + Save full page images seen in the WAL stream to the + given <replaceable>save_path</replaceable>, which should not exist or + should be empty. The images saved will be subject to the same + filtering and limiting criteria as display records, but in this + mode <application>pg_waldump</application> will not output any other + information. + </para> + <para> + The page images will be saved with the file + format: <literal><replaceable>LSN</replaceable>.<replaceable>TSOID</replaceable>.<replaceable>DBOID</replaceable>.<replaceable>RELNODE</replaceable>.<replaceable>BLKNO</replaceable></literal> + + The dot-separated components are (in order): + + <table id="pgwaldump-save-fpi-components"> + <tgroup cols="2"> + <thead> + <row> + <entry>Component</entry> + <entry>Description</entry> + </row> + </thead> + <tbody> + <row> + <entry>LSN</entry> + <entry>The LSN of the record with this block, formatted + as <literal>%08x-%08X</literal> instead of the + conventional <literal>%X/%X</literal> due to filesystem naming + limits</entry> + </row> + <row> + <entry>TSOID</entry> + <entry>tablespace OID for the block</entry> + </row> + <row> + <entry>DBOID</entry> + <entry>database OID for the block</entry> + </row> + <row> + <entry>RELNODE</entry> + <entry>relnode id for the block</entry> + </row> + <row> + <entry>BLKNO</entry> + <entry>the block number of this block</entry> + </row> + </tbody> + </tgroup> + </table> + </para> + </listitem> + </varlistentry> + <varlistentry> <term><option>-x <replaceable>xid</replaceable></option></term> <term><option>--xid=<replaceable>xid</replaceable></option></term> diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c index 4f265ef546..bcb3e2c350 100644 --- a/src/bin/pg_waldump/pg_waldump.c +++ b/src/bin/pg_waldump/pg_waldump.c @@ -26,6 +26,9 @@ #include "common/logging.h" #include "getopt_long.h" #include "rmgrdesc.h" +#include "storage/bufpage.h" +#include "storage/checksum.h" +#include "storage/checksum_impl.h" /* * NOTE: For any code change or issue fix here, it is highly recommended to @@ -70,6 +73,10 @@ typedef struct XLogDumpConfig bool filter_by_relation_block_enabled; ForkNumber filter_by_relation_forknum; bool filter_by_fpw; + + /* output options */ + bool save_fpw; + char *save_fpw_path; } XLogDumpConfig; @@ -439,6 +446,68 @@ XLogRecordHasFPW(XLogReaderState *record) return false; } +/* + * Function to externally save all FPWs stored in the given WAL record + */ +static void +XLogRecordSaveFPWs(XLogReaderState *record, const char *savepath) +{ + int block_id; + + for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) + { + /* we will now extract the fullpage image from the XLogRecord and save + * it to a calculated filename */ + + if (XLogRecHasBlockImage(record, block_id)) + { + char page[BLCKSZ]; + + memset(page, 0, BLCKSZ); + + if (RestoreBlockImage(record, block_id, page)) + { + /* we have our extracted FPI, let's save it now */ + char filename[MAXPGPATH]; + FILE *OPF; + BlockNumber blk; + RelFileNode rnode; + + XLogRecGetBlockTagExtended(record, block_id, + &rnode, NULL, &blk, NULL); + + /* + * The page may be uninitialized. If so, we can't set the LSN because + * that would corrupt the page. + */ + if (!PageIsNew(page)) + { + PageSetLSN(page, record->ReadRecPtr); + /* if checksum field is non-zero then we have checksums enabled, + * so recalculate the checksum with new LSN (yes, this is a hack) + */ + if (((PageHeader) page)->pd_checksum) + ((PageHeader) page)->pd_checksum = pg_checksum_page((char *) page, blk); + } + + /* TODO: do we need fork number here? */ + snprintf(filename, MAXPGPATH, "%s/%08X-%08X.%u.%u.%u.%u", savepath, + LSN_FORMAT_ARGS(record->ReadRecPtr), + rnode.spcNode, rnode.dbNode, rnode.relNode, blk); + + OPF = fopen(filename, PG_BINARY_W); + if (!OPF) + pg_fatal("Couldn't open file for output: %s", filename); + + if (pg_pwrite(fileno(OPF), page, BLCKSZ, 0) != BLCKSZ) + pg_fatal("Couldn't write out complete FPI to file: %s", filename); + + fclose(OPF); + } + } + } +} + /* * Print a record to stdout */ @@ -679,6 +748,7 @@ usage(void) " (default: 1 or the value used in STARTSEG)\n")); printf(_(" -V, --version output version information, then exit\n")); printf(_(" -w, --fullpage only show records with a full page write\n")); + printf(_(" -W, --raw-fpi=path save found full page images to given path\n")); printf(_(" -x, --xid=XID only show records with transaction ID XID\n")); printf(_(" -z, --stats[=record] show statistics instead of records\n" " (optionally, show per-record statistics)\n")); @@ -712,6 +782,7 @@ main(int argc, char **argv) {"limit", required_argument, NULL, 'n'}, {"path", required_argument, NULL, 'p'}, {"quiet", no_argument, NULL, 'q'}, + {"raw-fpi", required_argument, NULL, 'W'}, {"relation", required_argument, NULL, 'R'}, {"rmgr", required_argument, NULL, 'r'}, {"start", required_argument, NULL, 's'}, @@ -772,6 +843,8 @@ main(int argc, char **argv) config.filter_by_fpw = false; config.stats = false; config.stats_per_record = false; + config.save_fpw = false; + config.save_fpw_path = NULL; stats.startptr = InvalidXLogRecPtr; stats.endptr = InvalidXLogRecPtr; @@ -782,7 +855,7 @@ main(int argc, char **argv) goto bad_argument; } - while ((option = getopt_long(argc, argv, "bB:e:fF:n:p:qr:R:s:t:wx:z", + while ((option = getopt_long(argc, argv, "bB:e:fF:n:p:qr:R:s:t:wW:x:z", long_options, &optindex)) != -1) { switch (option) @@ -919,6 +992,10 @@ main(int argc, char **argv) case 'w': config.filter_by_fpw = true; break; + case 'W': + config.save_fpw = true; + config.save_fpw_path = pg_strdup(optarg); + break; case 'x': if (sscanf(optarg, "%u", &config.filter_by_xid) != 1) { @@ -972,6 +1049,54 @@ main(int argc, char **argv) } } + if (config.save_fpw_path != NULL) + { + struct stat st; + bool is_empty = false; + + /* we accept an empty existing directory */ + if (stat(config.save_fpw_path, &st) == 0 && S_ISDIR(st.st_mode)) + { + DIR *dir = opendir(config.save_fpw_path); + + if (dir) + { + struct dirent *d; + + is_empty = true; + while (errno = 0, (d = readdir(dir))) + { + if (strcmp(d->d_name, ".") != 0 && strcmp(d->d_name, "..") != 0) + { + is_empty = false; + break; + } + } + + if (errno) + { + pg_log_error("could not read raw-fpi directory \"%s\": %m", + config.save_fpw_path); + goto bad_argument; + } + + if (closedir(dir)) + { + pg_log_error("could not close raw-fpi directory \"%s\": %m", + config.save_fpw_path); + goto bad_argument; + } + } + } + if (!is_empty && mkdir(config.save_fpw_path, 0700) < 0) + { + pg_fatal("could not create raw-fpi output directory \"%s\": %m", + config.save_fpw_path); + + goto bad_argument; + } + } + /* parse files as start/end boundaries, extract path if not specified */ if (optind < argc) { @@ -1150,6 +1275,11 @@ main(int argc, char **argv) XLogRecStoreStats(&stats, xlogreader_state); stats.endptr = xlogreader_state->EndRecPtr; } + else if (config.save_fpw) + { + if (XLogRecordHasFPW(xlogreader_state)) + XLogRecordSaveFPWs(xlogreader_state, config.save_fpw_path); + } else XLogDumpDisplayRecord(&config, xlogreader_state); } -- 2.32.0 (Apple Git-132) ^ permalink raw reply [nested|flat] 71+ messages in thread
* Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL 2022-04-22 22:51 [PATCH] Teach pg_waldump to extract FPIs from the WAL David Christensen <[email protected]> @ 2022-04-23 14:49 ` Matthias van de Meent <[email protected]> 1 sibling, 0 replies; 71+ messages in thread From: Matthias van de Meent @ 2022-04-23 14:49 UTC (permalink / raw) To: David Christensen <[email protected]>; +Cc: pgsql-hackers On Sat, 23 Apr 2022 at 00:51, David Christensen <[email protected]> wrote: > > Hi -hackers, > > Enclosed is a patch to allow extraction/saving of FPI from the WAL > stream via pg_waldump. > > Description from the commit: > > Extracts full-page images from the WAL stream into a target directory, > which must be empty or not > exist. These images are subject to the same filtering rules as normal > display in pg_waldump, which > means that you can isolate the full page writes to a target relation, > among other things. > > Files are saved with the filename: <lsn>.<ts>.<db>.<rel>.<blk> with > formatting to make things Regardless of my (lack of) opinion on the inclusion of this patch in PG (I did not significantly review this patch); I noticed that you do not yet identify the 'fork' of the FPI in the file name. A lack of fork identifier in the exported file names would make debugging much more difficult due to the relatively difficult to identify data contained in !main forks, so I think this oversight should be fixed, be it through `_forkname` postfix like normal fork segments, or be it through `.<forknum>` numerical in- or postfix in the filename. -Matthias ^ permalink raw reply [nested|flat] 71+ messages in thread
* Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL 2022-04-22 22:51 [PATCH] Teach pg_waldump to extract FPIs from the WAL David Christensen <[email protected]> @ 2022-04-25 11:03 ` Bharath Rupireddy <[email protected]> 2022-04-25 15:24 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL David Christensen <[email protected]> 1 sibling, 1 reply; 71+ messages in thread From: Bharath Rupireddy @ 2022-04-25 11:03 UTC (permalink / raw) To: David Christensen <[email protected]>; +Cc: pgsql-hackers On Sat, Apr 23, 2022 at 4:21 AM David Christensen <[email protected]> wrote: > > Hi -hackers, > > Enclosed is a patch to allow extraction/saving of FPI from the WAL > stream via pg_waldump. > > Description from the commit: > > Extracts full-page images from the WAL stream into a target directory, > which must be empty or not > exist. These images are subject to the same filtering rules as normal > display in pg_waldump, which > means that you can isolate the full page writes to a target relation, > among other things. > > Files are saved with the filename: <lsn>.<ts>.<db>.<rel>.<blk> with > formatting to make things > somewhat sortable; for instance: > > 00000000-010000C0.1663.1.6117.0 > 00000000-01000150.1664.0.6115.0 > 00000000-010001E0.1664.0.6114.0 > 00000000-01000270.1663.1.6116.0 > 00000000-01000300.1663.1.6113.0 > 00000000-01000390.1663.1.6112.0 > 00000000-01000420.1663.1.8903.0 > 00000000-010004B0.1663.1.8902.0 > 00000000-01000540.1663.1.6111.0 > 00000000-010005D0.1663.1.6110.0 > > It's noteworthy that the raw images do not have the current LSN stored > with them in the WAL > stream (as would be true for on-heap versions of the blocks), nor > would the checksum be valid in > them (though WAL itself has checksums, so there is some protection > there). This patch chooses to > place the LSN and calculate the proper checksum (if non-zero in the > source image) in the outputted > block. (This could perhaps be a targetted flag if we decide we don't > always want this.) > > These images could be loaded/inspected via `pg_read_binary_file()` and > used in the `pageinspect` > suite of tools to perform detailed analysis on the pages in question, > based on historical > information, and may come in handy for forensics work. Thanks for working on this. I'm just thinking if we can use these FPIs to repair the corrupted pages? I would like to understand more detailed usages of the FPIs other than inspecting with pageinspect. Given that others have realistic use-cases (of course I would like to know more about those), +1 for the idea. However, I would suggest adding a function to extract raw FPI data to the pg_walinspect extension that got recently committed in PG 15, the out of which can directly be fed to pageinspect functions or Few comments: 1) I think it's good to mention the stored file name format. + printf(_(" -W, --raw-fpi=path save found full page images to given path\n")); 2) + for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) + { + /* we will now extract the fullpage image from the XLogRecord and save + * it to a calculated filename */ + + if (XLogRecHasBlockImage(record, block_id)) I think we need XLogRecHasBlockRef to be true to check XLogRecHasBlockImage otherwise, we will see some build farms failing, recently I've seen this failure for pg_walinspect.. for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) { if (!XLogRecHasBlockRef(record, block_id)) continue; if (XLogRecHasBlockImage(record, block_id)) *fpi_len += XLogRecGetBlock(record, block_id)->bimg_len; } 3) Please correct the commenting format: + /* we will now extract the fullpage image from the XLogRecord and save + * it to a calculated filename */ 4) Usually we start errors with lower case letters "could not ....." + pg_fatal("Couldn't open file for output: %s", filename); + pg_fatal("Couldn't write out complete FPI to file: %s", filename); And the variable name too: + FILE *OPF; 5) Not sure how the FPIs of TOASTed tables get stored, but it would be good to check. 6) Good to specify the known usages of FPIs in the documentation. 7) Isn't it good to emit an error if RestoreBlockImage returns false? + if (RestoreBlockImage(record, block_id, page)) + { 8) I think I don't mind if a non-empty directory is specified - IMO better usability is this - if the directory is non-empty, just go add the FPI files if FPI file exists just replace it, if the directory isn't existing, create and write the FPI files. + /* we accept an empty existing directory */ + if (stat(config.save_fpw_path, &st) == 0 && S_ISDIR(st.st_mode)) + { 9) Instead of following: + if (XLogRecordHasFPW(xlogreader_state)) + XLogRecordSaveFPWs(xlogreader_state, config.save_fpw_path); I will just do this in XLogRecordSaveFPWs: for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) { if (!XLogRecHasBlockRef(record, block_id)) continue; if (XLogRecHasBlockImage(record, block_id)) { } } 10) Along with pg_pwrite(), can we also fsync the files (of course users can choose it optionally) so that the writes will be durable for the OS crashes? Regards, Bharath Rupireddy. ^ permalink raw reply [nested|flat] 71+ messages in thread
* Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL 2022-04-22 22:51 [PATCH] Teach pg_waldump to extract FPIs from the WAL David Christensen <[email protected]> 2022-04-25 11:03 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL Bharath Rupireddy <[email protected]> @ 2022-04-25 15:24 ` David Christensen <[email protected]> 2022-04-26 02:42 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL Michael Paquier <[email protected]> 0 siblings, 1 reply; 71+ messages in thread From: David Christensen @ 2022-04-25 15:24 UTC (permalink / raw) To: Bharath Rupireddy <[email protected]>; +Cc: pgsql-hackers On Mon, Apr 25, 2022 at 6:03 AM Bharath Rupireddy <[email protected]> wrote: > Thanks for working on this. I'm just thinking if we can use these FPIs > to repair the corrupted pages? I would like to understand more > detailed usages of the FPIs other than inspecting with pageinspect. My main use case was for being able to look at potential corruption, either in the WAL stream, on heap, or in tools associated with the WAL stream. I suppose you could use the page images to replace corrupted on-disk pages (and in fact I think I've heard of a tool or two that try to do that), though don't know that I consider this the primary purpose (and having toast tables and the list, as well as clog would make it potentially hard to just drop-in a page version without issues). Might help in extreme situations though. > Given that others have realistic use-cases (of course I would like to > know more about those), +1 for the idea. However, I would suggest > adding a function to extract raw FPI data to the pg_walinspect > extension that got recently committed in PG 15, the out of which can > directly be fed to pageinspect functions or Yeah, makes sense to have some overlap here; will review what is there and see if there is some shared code base we can utilize. (ISTR some work towards getting these two tools using more of the same code, and this seems like another such instance.) > Few comments: > 1) I think it's good to mention the stored file name format. > + printf(_(" -W, --raw-fpi=path save found full page images to > given path\n")); +1, though I've also thought there could be uses to have multiple possible output formats here (most immediately, there may be cases where we want *each* FPI for a block vs the *latest*, so files name with/without the LSN component seem the easiest way forward here). That would introduce some additional complexity though, so might need to see if others think that makes any sense. > 2) > + for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) > + { > + /* we will now extract the fullpage image from the XLogRecord and save > + * it to a calculated filename */ > + > + if (XLogRecHasBlockImage(record, block_id)) > > I think we need XLogRecHasBlockRef to be true to check > XLogRecHasBlockImage otherwise, we will see some build farms failing, > recently I've seen this failure for pg_walinspect.. > > for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) > { > if (!XLogRecHasBlockRef(record, block_id)) > continue; > > if (XLogRecHasBlockImage(record, block_id)) > *fpi_len += XLogRecGetBlock(record, block_id)->bimg_len; > } Good point; my previous patch that got committed here (127aea2a65) probably also needed this treatment. > 3) Please correct the commenting format: > + /* we will now extract the fullpage image from the XLogRecord and save > + * it to a calculated filename */ Ack. > 4) Usually we start errors with lower case letters "could not ....." > + pg_fatal("Couldn't open file for output: %s", filename); > + pg_fatal("Couldn't write out complete FPI to file: %s", filename); > And the variable name too: > + FILE *OPF; Ack. > 5) Not sure how the FPIs of TOASTed tables get stored, but it would be > good to check. What would be different here? Are there issues you can think of, or just more from the pageinspect side of things? > 6) Good to specify the known usages of FPIs in the documentation. Ack. Prob good to get additional info/use cases from others, as mine is fairly short. :-) > 7) Isn't it good to emit an error if RestoreBlockImage returns false? > + if (RestoreBlockImage(record, block_id, page)) > + { Ack. > 8) I think I don't mind if a non-empty directory is specified - IMO > better usability is this - if the directory is non-empty, just go add > the FPI files if FPI file exists just replace it, if the directory > isn't existing, create and write the FPI files. > + /* we accept an empty existing directory */ > + if (stat(config.save_fpw_path, &st) == 0 && S_ISDIR(st.st_mode)) > + { Agreed; was mainly trying to prevent accidental expansion inside `pg_wal` when an earlier version of the patch implied `.` as the current dir with an optional path, but I've since made the path non-optional and agree that this is unnecessarily restrictive. > 9) Instead of following: > + if (XLogRecordHasFPW(xlogreader_state)) > + XLogRecordSaveFPWs(xlogreader_state, config.save_fpw_path); > I will just do this in XLogRecordSaveFPWs: > for (block_id = 0; block_id <= XLogRecMaxBlockId(record); block_id++) > { > if (!XLogRecHasBlockRef(record, block_id)) > continue; > > if (XLogRecHasBlockImage(record, block_id)) > { > > } > } Yeah, a little redundant. > 10) Along with pg_pwrite(), can we also fsync the files (of course > users can choose it optionally) so that the writes will be durable for > the OS crashes? Can add; you thinking a separate flag to disable this with default true? Best, David ^ permalink raw reply [nested|flat] 71+ messages in thread
* Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL 2022-04-22 22:51 [PATCH] Teach pg_waldump to extract FPIs from the WAL David Christensen <[email protected]> 2022-04-25 11:03 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL Bharath Rupireddy <[email protected]> 2022-04-25 15:24 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL David Christensen <[email protected]> @ 2022-04-26 02:42 ` Michael Paquier <[email protected]> 0 siblings, 0 replies; 71+ messages in thread From: Michael Paquier @ 2022-04-26 02:42 UTC (permalink / raw) To: David Christensen <[email protected]>; +Cc: Bharath Rupireddy <[email protected]>; pgsql-hackers On Mon, Apr 25, 2022 at 10:24:52AM -0500, David Christensen wrote: > On Mon, Apr 25, 2022 at 6:03 AM Bharath Rupireddy > <[email protected]> wrote: >> Thanks for working on this. I'm just thinking if we can use these FPIs >> to repair the corrupted pages? I would like to understand more >> detailed usages of the FPIs other than inspecting with pageinspect. > > My main use case was for being able to look at potential corruption, > either in the WAL stream, on heap, or in tools associated with the WAL > stream. I suppose you could use the page images to replace corrupted > on-disk pages (and in fact I think I've heard of a tool or two that > try to do that), though don't know that I consider this the primary > purpose (and having toast tables and the list, as well as clog would > make it potentially hard to just drop-in a page version without > issues). Might help in extreme situations though. You could do a bunch of things with those images, even make things worse if you are not careful enough. >> 10) Along with pg_pwrite(), can we also fsync the files (of course >> users can choose it optionally) so that the writes will be durable for >> the OS crashes? > > Can add; you thinking a separate flag to disable this with default true? We expect data generated by tools like pg_dump, pg_receivewal (depending on the use --synchronous) or pg_basebackup to be consistent when we exit from the call. FWIW, flushing this data does not seem like a strong requirement for something aimed at being used page-level chirurgy or lookups, because the WAL segments should still be around even if the host holding the archives is unplugged. -- Michael Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 71+ messages in thread
end of thread, other threads:[~2022-04-26 02:42 UTC | newest] Thread overview: 71+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]> 2022-04-22 22:51 [PATCH] Teach pg_waldump to extract FPIs from the WAL David Christensen <[email protected]> 2022-04-23 14:49 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL Matthias van de Meent <[email protected]> 2022-04-25 11:03 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL Bharath Rupireddy <[email protected]> 2022-04-25 15:24 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL David Christensen <[email protected]> 2022-04-26 02:42 ` Re: [PATCH] Teach pg_waldump to extract FPIs from the WAL Michael Paquier <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox