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 / 4 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 | 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 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 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 | 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 | 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 | 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 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 | 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 | 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 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 | 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 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 | 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 | 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 | 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 | 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 | 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
* Re: Adding locks statistics
@ 2026-02-13 07:36 Michael Paquier <[email protected]>
2026-02-13 10:24 ` Re: Adding locks statistics Bertrand Drouvot <[email protected]>
0 siblings, 1 reply; 71+ messages in thread
From: Michael Paquier @ 2026-02-13 07:36 UTC (permalink / raw)
To: Bertrand Drouvot <[email protected]>; +Cc: Jeff Davis <[email protected]>; Greg Sabino Mullane <[email protected]>; [email protected]
On Tue, Feb 10, 2026 at 07:30:50AM +0000, Bertrand Drouvot wrote:
> New rebase due to 73d60ac385a.
I have been looking at this patch, and can get behind the data
gathered here in terms of being able to tune things, but not all. See
below for the details of my reasoning.
max_locks_per_xact is a PGC_POSTMASTER, so backend-level stats with
this data would not be relevant for its tuning. However, something
else can be said about deadlock_timeout, where one could rely on the
data gathered by this view to set it on a backend basis, particularly
if the load pattern is divided into a subsets of connections (say few
backend see a lot of the deadlock_timeout, for example). Same
argument for lock_timeout, which is user-settable.
As the set of data gathered, I think that I'm OK with timeouts (for
lock_timeout), deadlock_timeouts (for deadlock_timeout), fastpath (for
max_locks_per_xact), that can all be compared with the number of
requests.
Regarding "deadlocks" and "waits", these two are less useful than the
three others because not really actionable. They would become much
more relevant if and only if we know the distribution of the deadlocks
not only for the lock types, but for the objects involved, especially
if the activity is diluted across many objects. So these have less
value IMO because they are not really actionable in the system. The
other three can be directly tuned based on the GUCs we have.
So my suggestion for the moment would be to be more frugal (yeah I
know, sorry..) and limit ourselves to four fields: deadlock_timeout,
requests, fastpath and timeouts. Three fields to compare with
requests, one for each GUC.
Regarding the implementation, you are right to use a fixed-sized stats
kind for the job. I can see a lot of code has been copy-pasted from
pgstat_io.c, then slightly adjusted to fit into the picture. That's
fine here, it makes the implementation straight-forward to read.
Regarding the documentation, listing all the values for locktype is a
recipe for rot. I'd suggest to remove the list instead, with only a
link referring to pg_locks to avoid the duplication.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 71+ messages in thread
* Re: Adding locks statistics
2026-02-13 07:36 Re: Adding locks statistics Michael Paquier <[email protected]>
@ 2026-02-13 10:24 ` Bertrand Drouvot <[email protected]>
2026-02-13 21:13 ` Re: Adding locks statistics Andres Freund <[email protected]>
0 siblings, 1 reply; 71+ messages in thread
From: Bertrand Drouvot @ 2026-02-13 10:24 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jeff Davis <[email protected]>; Greg Sabino Mullane <[email protected]>; [email protected]
Hi,
On Fri, Feb 13, 2026 at 04:36:57PM +0900, Michael Paquier wrote:
> On Tue, Feb 10, 2026 at 07:30:50AM +0000, Bertrand Drouvot wrote:
> > New rebase due to 73d60ac385a.
>
> I have been looking at this patch, and can get behind the data
> gathered here in terms of being able to tune things
Thanks!
>
> So my suggestion for the moment would be to be more frugal (yeah I
> know, sorry..) and limit ourselves to four fields: deadlock_timeout,
> requests, fastpath and timeouts. Three fields to compare with
> requests, one for each GUC.
That's fine by me. We could still add the others in the future if we feel the
need. Done that way in the attached.
> Regarding the implementation, you are right to use a fixed-sized stats
> kind for the job. I can see a lot of code has been copy-pasted from
> pgstat_io.c, then slightly adjusted to fit into the picture. That's
> fine here, it makes the implementation straight-forward to read.
Yeah, no need to reinvent the wheel.
> Regarding the documentation, listing all the values for locktype is a
> recipe for rot. I'd suggest to remove the list instead, with only a
> link referring to pg_locks to avoid the duplication.
Makes sense, done that way. Makes me think that we could do the same for pg_locks
and just link it to the Wait Events of Type Lock? (Table 27.11.)
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v4-0001-Add-lock-statistics.patch (14.9K, ../../[email protected]/2-v4-0001-Add-lock-statistics.patch)
download | inline diff:
From 7e7f2cecfedce71c5aa68ac6843a003540ee33e2 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <[email protected]>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v4 1/2] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
requests
timeouts
deadlock_timeouts
fastpath
These counters enable tuning of lock related GUCs (max_locks_per_transaction,
lock_timeout, and deadlock_timeout).
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 12 ++
src/backend/storage/lmgr/proc.c | 2 +
src/backend/tcop/postgres.c | 8 ++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 160 +++++++++++++++++++++++
src/include/pgstat.h | 30 +++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
11 files changed, 260 insertions(+), 2 deletions(-)
9.7% src/backend/storage/lmgr/
3.0% src/backend/tcop/
66.2% src/backend/utils/activity/
9.1% src/include/utils/
10.9% src/include/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index e1168ad3837..30537ccfade 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -45,6 +45,7 @@
#include "storage/spin.h"
#include "storage/standby.h"
#include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
#include "utils/ps_status.h"
#include "utils/resowner.h"
@@ -871,6 +872,9 @@ LockAcquireExtended(const LOCKTAG *locktag,
lockMethodTable->lockModeNames[lockmode]),
errhint("Only RowExclusiveLock or less can be acquired on database objects during recovery.")));
+ /* Increment the lock statistics requests counter */
+ pgstat_count_lock_requests(locktag->locktag_type);
+
#ifdef LOCK_DEBUG
if (LOCK_DEBUG_ENABLED(locktag))
elog(LOG, "LockAcquire: lock [%u,%u] %s",
@@ -2799,6 +2803,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
Assert(!FAST_PATH_CHECK_LOCKMODE(MyProc, f, lockmode));
FAST_PATH_SET_LOCKMODE(MyProc, f, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
return true;
}
}
@@ -2808,6 +2814,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
MyProc->fpRelId[unused_slot] = relid;
FAST_PATH_SET_LOCKMODE(MyProc, unused_slot, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
++FastPathLocalUseCounts[group];
return true;
}
@@ -4629,6 +4637,10 @@ VirtualXactLockTableInsert(VirtualTransactionId vxid)
MyProc->fpVXIDLock = true;
MyProc->fpLocalTransactionId = vxid.localTransactionId;
+ /* Increment the lock statistics requests and fastpath counters */
+ pgstat_count_lock_requests(LOCKTAG_VIRTUALTRANSACTION);
+ pgstat_count_lock_fastpath(LOCKTAG_VIRTUALTRANSACTION);
+
LWLockRelease(&MyProc->fpInfoLock);
}
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index fd8318bdf3d..5eee8d1f69d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1447,6 +1447,8 @@ ProcSleep(LOCALLOCK *locallock)
/* check for deadlocks first, as that's probably log-worthy */
if (got_deadlock_timeout)
{
+ /* Increment the lock statistics deadlock_timeouts counter */
+ pgstat_count_lock_deadlock_timeouts(locallock->tag.lock.locktag_type);
deadlock_state = CheckDeadLock();
got_deadlock_timeout = false;
}
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21de158adbb..eadb43a283d 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3472,6 +3472,14 @@ ProcessInterrupts(void)
if (lock_timeout_occurred)
{
+ LOCALLOCK *lockAwaited;
+
+ lockAwaited = GetAwaitedLock();
+
+ /* Increment the lock statistics timeouts counter */
+ if (lockAwaited)
+ pgstat_count_lock_timeouts(lockAwaited->tag.lock.locktag_type);
+
LockErrorCleanup();
ereport(ERROR,
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -26,6 +26,7 @@ OBJS = \
pgstat_database.o \
pgstat_function.o \
pgstat_io.o \
+ pgstat_lock.o \
pgstat_relation.o \
pgstat_replslot.o \
pgstat_shmem.o \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
'pgstat_database.c',
'pgstat_function.c',
'pgstat_io.c',
+ 'pgstat_lock.c',
'pgstat_relation.c',
'pgstat_replslot.c',
'pgstat_shmem.c',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -83,6 +83,7 @@
* - pgstat_database.c
* - pgstat_function.c
* - pgstat_io.c
+ * - pgstat_lock.c
* - pgstat_relation.c
* - pgstat_replslot.c
* - pgstat_slru.c
@@ -448,6 +449,23 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
.snapshot_cb = pgstat_io_snapshot_cb,
},
+ [PGSTAT_KIND_LOCK] = {
+ .name = "lock",
+
+ .fixed_amount = true,
+ .write_to_file = true,
+
+ .snapshot_ctl_off = offsetof(PgStat_Snapshot, lock),
+ .shared_ctl_off = offsetof(PgStat_ShmemControl, lock),
+ .shared_data_off = offsetof(PgStatShared_Lock, stats),
+ .shared_data_len = sizeof(((PgStatShared_Lock *) 0)->stats),
+
+ .flush_static_cb = pgstat_lock_flush_cb,
+ .init_shmem_cb = pgstat_lock_init_shmem_cb,
+ .reset_all_cb = pgstat_lock_reset_all_cb,
+ .snapshot_cb = pgstat_lock_snapshot_cb,
+ },
+
[PGSTAT_KIND_SLRU] = {
.name = "slru",
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
new file mode 100644
index 00000000000..110a370b875
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,160 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_lock.c
+ * Implementation of lock statistics.
+ *
+ * This file contains the implementation of lock statistics. It is kept separate
+ * from pgstat.c to enforce the line between the statistics access / storage
+ * implementation and the details about individual types of statistics.
+ *
+ * Copyright (c) 2021-2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/backend/utils/activity/pgstat_lock.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "utils/pgstat_internal.h"
+
+static PgStat_PendingLock PendingLockStats;
+static bool have_lockstats = false;
+
+/*
+ * Simpler wrapper of pgstat_lock_flush_cb()
+ */
+void
+pgstat_lock_flush(bool nowait)
+{
+ (void) pgstat_lock_flush_cb(nowait);
+}
+
+/*
+ * Flush out locally pending lock statistics
+ *
+ * If no stats have been recorded, this function returns false.
+ *
+ * If nowait is true, this function returns true if the lock could not be
+ * acquired. Otherwise, return false.
+ */
+bool
+pgstat_lock_flush_cb(bool nowait)
+{
+ LWLock *lcktype_lock;
+ PgStat_LockEntry *lck_shstats;
+ bool lock_not_acquired = false;
+
+ if (!have_lockstats)
+ return false;
+
+ for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+ {
+ lcktype_lock = &pgStatLocal.shmem->lock.locks[i];
+ lck_shstats =
+ &pgStatLocal.shmem->lock.stats.stats[i];
+
+ if (!nowait)
+ LWLockAcquire(lcktype_lock, LW_EXCLUSIVE);
+ else if (!LWLockConditionalAcquire(lcktype_lock, LW_EXCLUSIVE))
+ {
+ lock_not_acquired = true;
+ continue;
+ }
+
+#define LOCKSTAT_ACC(fld) \
+ (lck_shstats->fld += PendingLockStats.stats[i].fld)
+ LOCKSTAT_ACC(requests);
+ LOCKSTAT_ACC(timeouts);
+ LOCKSTAT_ACC(deadlock_timeouts);
+ LOCKSTAT_ACC(fastpath);
+#undef LOCKSTAT_ACC
+
+ LWLockRelease(lcktype_lock);
+ }
+
+ memset(&PendingLockStats, 0, sizeof(PendingLockStats));
+
+ have_lockstats = false;
+
+ return lock_not_acquired;
+}
+
+
+void
+pgstat_lock_init_shmem_cb(void *stats)
+{
+ PgStatShared_Lock *stat_shmem = (PgStatShared_Lock *) stats;
+
+ for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+ LWLockInitialize(&stat_shmem->locks[i], LWTRANCHE_PGSTATS_DATA);
+}
+
+void
+pgstat_lock_reset_all_cb(TimestampTz ts)
+{
+ for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+ {
+ LWLock *lcktype_lock = &pgStatLocal.shmem->lock.locks[i];
+ PgStat_LockEntry *lck_shstats = &pgStatLocal.shmem->lock.stats.stats[i];
+
+ LWLockAcquire(lcktype_lock, LW_EXCLUSIVE);
+
+ /*
+ * Use the lock in the first lock type PgStat_LockEntry to protect the
+ * reset timestamp as well.
+ */
+ if (i == 0)
+ pgStatLocal.shmem->lock.stats.stat_reset_timestamp = ts;
+
+ memset(lck_shstats, 0, sizeof(*lck_shstats));
+ LWLockRelease(lcktype_lock);
+ }
+}
+
+void
+pgstat_lock_snapshot_cb(void)
+{
+ for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+ {
+ LWLock *lcktype_lock = &pgStatLocal.shmem->lock.locks[i];
+ PgStat_LockEntry *lck_shstats = &pgStatLocal.shmem->lock.stats.stats[i];
+ PgStat_LockEntry *lck_snap = &pgStatLocal.snapshot.lock.stats[i];
+
+ LWLockAcquire(lcktype_lock, LW_SHARED);
+
+ /*
+ * Use the lock in the first lock type PgStat_LockEntry to protect the
+ * reset timestamp as well.
+ */
+ if (i == 0)
+ pgStatLocal.snapshot.lock.stat_reset_timestamp =
+ pgStatLocal.shmem->lock.stats.stat_reset_timestamp;
+
+ /* using struct assignment due to better type safety */
+ *lck_snap = *lck_shstats;
+ LWLockRelease(lcktype_lock);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_requests */
+PGSTAT_COUNT_LOCK_FUNC(requests)
+
+/* pgstat_count_lock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(timeouts)
+
+/* pgstat_count_lock_deadlock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(deadlock_timeouts)
+
+/* pgstat_count_lock_fastpath */
+PGSTAT_COUNT_LOCK_FUNC(fastpath)
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index fff7ecc2533..8460e2db159 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -17,6 +17,7 @@
#include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
#include "replication/conflict.h"
#include "replication/worker_internal.h"
+#include "storage/lock.h"
#include "utils/backend_progress.h" /* for backward compatibility */ /* IWYU pragma: export */
#include "utils/backend_status.h" /* for backward compatibility */ /* IWYU pragma: export */
#include "utils/pgstat_kind.h"
@@ -342,6 +343,25 @@ typedef struct PgStat_IO
PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
} PgStat_IO;
+typedef struct PgStat_LockEntry
+{
+ PgStat_Counter requests;
+ PgStat_Counter timeouts;
+ PgStat_Counter deadlock_timeouts;
+ PgStat_Counter fastpath;
+} PgStat_LockEntry;
+
+typedef struct PgStat_PendingLock
+{
+ PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1];
+} PgStat_PendingLock;
+
+typedef struct PgStat_Lock
+{
+ TimestampTz stat_reset_timestamp;
+ PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1];
+} PgStat_Lock;
+
typedef struct PgStat_StatDBEntry
{
PgStat_Counter xact_commit;
@@ -614,6 +634,16 @@ extern bool pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
IOContext io_context, IOOp io_op);
+/*
+ * Functions in pgstat_lock.c
+ */
+
+extern void pgstat_lock_flush(bool nowait);
+extern void pgstat_count_lock_requests(uint8 locktag_type);
+extern void pgstat_count_lock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -464,6 +464,16 @@ typedef struct PgStatShared_IO
PgStat_IO stats;
} PgStatShared_IO;
+typedef struct PgStatShared_Lock
+{
+ /*
+ * locks[i] protects stats.stats[i]. locks[0] also protects
+ * stats.stat_reset_timestamp.
+ */
+ LWLock locks[LOCKTAG_LAST_TYPE + 1];
+ PgStat_Lock stats;
+} PgStatShared_Lock;
+
typedef struct PgStatShared_SLRU
{
/* lock protects ->stats */
@@ -570,6 +580,7 @@ typedef struct PgStat_ShmemControl
PgStatShared_BgWriter bgwriter;
PgStatShared_Checkpointer checkpointer;
PgStatShared_IO io;
+ PgStatShared_Lock lock;
PgStatShared_SLRU slru;
PgStatShared_Wal wal;
@@ -602,6 +613,8 @@ typedef struct PgStat_Snapshot
PgStat_IO io;
+ PgStat_Lock lock;
+
PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS];
PgStat_WalStats wal;
@@ -752,6 +765,14 @@ extern void pgstat_io_init_shmem_cb(void *stats);
extern void pgstat_io_reset_all_cb(TimestampTz ts);
extern void pgstat_io_snapshot_cb(void);
+/*
+ * Functions in pgstat_lock.c
+ */
+
+extern bool pgstat_lock_flush_cb(bool nowait);
+extern void pgstat_lock_init_shmem_cb(void *stats);
+extern void pgstat_lock_reset_all_cb(TimestampTz ts);
+extern void pgstat_lock_snapshot_cb(void);
/*
* Functions in pgstat_relation.c
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -36,8 +36,9 @@
#define PGSTAT_KIND_BGWRITER 8
#define PGSTAT_KIND_CHECKPOINTER 9
#define PGSTAT_KIND_IO 10
-#define PGSTAT_KIND_SLRU 11
-#define PGSTAT_KIND_WAL 12
+#define PGSTAT_KIND_LOCK 11
+#define PGSTAT_KIND_SLRU 12
+#define PGSTAT_KIND_WAL 13
#define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE
#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6e2d876a40f..cd3688524af 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2255,6 +2255,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2277,8 +2278,11 @@ PgStat_HashKey
PgStat_IO
PgStat_KindInfo
PgStat_LocalState
+PgStat_Lock
+PgStat_LockEntry
PgStat_PendingDroppedStatsItem
PgStat_PendingIO
+PgStat_PendingLock
PgStat_SLRUStats
PgStat_ShmemControl
PgStat_Snapshot
--
2.34.1
[text/x-diff] v4-0002-Add-the-pg_stat_lock-view.patch (15.9K, ../../[email protected]/3-v4-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From f7b2a14597830286b08bc8a78722c96fc73cafd6 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <[email protected]>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v4 2/2] Add the pg_stat_lock view
This new view reports lock statistics.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 117 ++++++++++++++++++
src/backend/catalog/system_views.sql | 10 ++
src/backend/utils/activity/pgstat_lock.c | 8 ++
src/backend/utils/adt/pgstatfuncs.c | 40 ++++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/deadlock-hard.out | 20 ++-
src/test/isolation/specs/deadlock-hard.spec | 5 +-
src/test/regress/expected/advisory_lock.out | 18 +++
src/test/regress/expected/rules.out | 7 ++
src/test/regress/sql/advisory_lock.sql | 4 +
11 files changed, 237 insertions(+), 2 deletions(-)
46.2% doc/src/sgml/
3.2% src/backend/catalog/
16.3% src/backend/utils/adt/
6.2% src/include/catalog/
9.3% src/test/isolation/expected/
6.7% src/test/isolation/specs/
7.3% src/test/regress/expected/
4.4% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b77d189a500..1cba84c1542 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,15 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser
</entry>
</row>
+ <row>
+ <entry><structname>pg_stat_lock</structname><indexterm><primary>pg_stat_lock</primary></indexterm></entry>
+ <entry>
+ One row for each lock type, containing cluster-wide locks statistics.
+ See <link linkend="monitoring-pg-stat-lock-view">
+ <structname>pg_stat_lock</structname></link> for details.
+ </entry>
+ </row>
+
<row>
<entry><structname>pg_stat_replication_slots</structname><indexterm><primary>pg_stat_replication_slots</primary></indexterm></entry>
<entry>One row per replication slot, showing statistics about the
@@ -3124,6 +3133,108 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</sect2>
+
+ <sect2 id="monitoring-pg-stat-lock-view">
+ <title><structname>pg_stat_lock</structname></title>
+
+ <indexterm>
+ <primary>pg_stat_lock</primary>
+ </indexterm>
+
+ <para>
+ The <structname>pg_stat_lock</structname> view will contain one row for each
+ lock type, showing cluster-wide locks statistics.
+ </para>
+
+ <table id="pg-stat-lock-view" xreflabel="pg_stat_lock">
+ <title><structname>pg_stat_lock</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para>
+ </entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>locktype</structfield> <type>text</type>
+ </para>
+ <para>
+ Type of the lockable object. See <link linkend="view-pg-locks">
+ <structname>pg_stat_lock</structname></link> for details.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>requests</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of requests for this lock type.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>lock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>deadlock_timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>deadlock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>fastpath</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times this lock type was taken via fast path.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
+ </para>
+ <para>
+ Time at which these statistics were last reset.
+ </para>
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect2>
+
<sect2 id="monitoring-pg-stat-bgwriter-view">
<title><structname>pg_stat_bgwriter</structname></title>
@@ -5195,6 +5306,12 @@ description | Waiting for a newly initialized WAL file to reach durable storage
<structname>pg_stat_io</structname> view.
</para>
</listitem>
+ <listitem>
+ <para>
+ <literal>lock</literal>: Reset all the counters shown in the
+ <structname>pg_stat_lock</structname> view.
+ </para>
+ </listitem>
<listitem>
<para>
<literal>recovery_prefetch</literal>: Reset all the counters shown in
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ea8f1faa9e..a51ae67de05 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -985,6 +985,16 @@ CREATE VIEW pg_stat_slru AS
s.stats_reset
FROM pg_stat_get_slru() s;
+CREATE VIEW pg_stat_lock AS
+ SELECT
+ l.locktype,
+ l.requests,
+ l.timeouts,
+ l.deadlock_timeouts,
+ l.fastpath,
+ l.stats_reset
+ FROM pg_stat_get_lock() l;
+
CREATE VIEW pg_stat_wal_receiver AS
SELECT
s.pid,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index 110a370b875..e26e24c260c 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
static PgStat_PendingLock PendingLockStats;
static bool have_lockstats = false;
+PgStat_Lock *
+pgstat_fetch_stat_lock(void)
+{
+ pgstat_snapshot_fixed(PGSTAT_KIND_LOCK);
+
+ return &pgStatLocal.snapshot.lock;
+}
+
/*
* Simpler wrapper of pgstat_lock_flush_cb()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b1df96e7b0b..6a699da8bf0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,6 +1737,43 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
wal_stats->stat_reset_timestamp));
}
+Datum
+pg_stat_get_lock(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_LOCK_COLS 6
+ ReturnSetInfo *rsinfo;
+ PgStat_Lock *lock_stats;
+
+ InitMaterializedSRF(fcinfo, 0);
+ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ lock_stats = pgstat_fetch_stat_lock();
+
+ for (int lcktype = 0; lcktype <= LOCKTAG_LAST_TYPE; lcktype++)
+ {
+ const char *locktypename;
+ Datum values[PG_STAT_LOCK_COLS] = {0};
+ bool nulls[PG_STAT_LOCK_COLS] = {0};
+ PgStat_LockEntry *lck_stats = &lock_stats->stats[lcktype];
+ int i = 0;
+
+ locktypename = LockTagTypeNames[lcktype];
+
+ values[i++] = CStringGetTextDatum(locktypename);
+ values[i++] = Int64GetDatum(lck_stats->requests);
+ values[i++] = Int64GetDatum(lck_stats->timeouts);
+ values[i++] = Int64GetDatum(lck_stats->deadlock_timeouts);
+ values[i++] = Int64GetDatum(lck_stats->fastpath);
+ values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+
+ Assert(i + 1 == PG_STAT_LOCK_COLS);
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ return (Datum) 0;
+}
+
/*
* Returns statistics of SLRU caches.
*/
@@ -1921,6 +1958,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
pgstat_reset_of_kind(PGSTAT_KIND_BGWRITER);
pgstat_reset_of_kind(PGSTAT_KIND_CHECKPOINTER);
pgstat_reset_of_kind(PGSTAT_KIND_IO);
+ pgstat_reset_of_kind(PGSTAT_KIND_LOCK);
XLogPrefetchResetStats();
pgstat_reset_of_kind(PGSTAT_KIND_SLRU);
pgstat_reset_of_kind(PGSTAT_KIND_WAL);
@@ -1938,6 +1976,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
pgstat_reset_of_kind(PGSTAT_KIND_CHECKPOINTER);
else if (strcmp(target, "io") == 0)
pgstat_reset_of_kind(PGSTAT_KIND_IO);
+ else if (strcmp(target, "lock") == 0)
+ pgstat_reset_of_kind(PGSTAT_KIND_LOCK);
else if (strcmp(target, "recovery_prefetch") == 0)
XLogPrefetchResetStats();
else if (strcmp(target, "slru") == 0)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..e208ebdbbc3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6026,6 +6026,15 @@
proargnames => '{backend_type,object,context,reads,read_bytes,read_time,writes,write_bytes,write_time,writebacks,writeback_time,extends,extend_bytes,extend_time,hits,evictions,reuses,fsyncs,fsync_time,stats_reset}',
prosrc => 'pg_stat_get_io' },
+{ oid => '9375', descr => 'statistics: per lock type statistics',
+ proname => 'pg_stat_get_lock', prorows => '10', proretset => 't',
+ provolatile => 'v', proparallel => 'r', prorettype => 'record',
+ proargtypes => '',
+ proallargtypes => '{text,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{o,o,o,o,o,o}',
+ proargnames => '{locktype,requests,timeouts,deadlock_timeouts,fastpath,stats_reset}',
+ prosrc => 'pg_stat_get_lock' },
+
{ oid => '6386', descr => 'statistics: backend IO statistics',
proname => 'pg_stat_get_backend_io', prorows => '5', proretset => 't',
provolatile => 'v', proparallel => 'r', prorettype => 'record',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8460e2db159..b06a261572b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -643,6 +643,7 @@ extern void pgstat_count_lock_requests(uint8 locktag_type);
extern void pgstat_count_lock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/deadlock-hard.out b/src/test/isolation/expected/deadlock-hard.out
index 460653f2b86..5ae5a8bf953 100644
--- a/src/test/isolation/expected/deadlock-hard.out
+++ b/src/test/isolation/expected/deadlock-hard.out
@@ -1,6 +1,12 @@
Parsed test spec with 8 sessions
-starting permutation: s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s7c s6c s5c s4c s3c s2c s1c
+starting permutation: s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
+step s1rl: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+
+(1 row)
+
step s1a1: LOCK TABLE a1;
step s2a2: LOCK TABLE a2;
step s3a3: LOCK TABLE a3;
@@ -21,6 +27,12 @@ step s8a1: <... completed>
ERROR: deadlock detected
step s7a8: <... completed>
step s8c: COMMIT;
+step s8f: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
step s7c: COMMIT;
step s6a7: <... completed>
step s6c: COMMIT;
@@ -34,3 +46,9 @@ step s2a3: <... completed>
step s2c: COMMIT;
step s1a2: <... completed>
step s1c: COMMIT;
+step s1sl: SELECT deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?
+--------
+t
+(1 row)
+
diff --git a/src/test/isolation/specs/deadlock-hard.spec b/src/test/isolation/specs/deadlock-hard.spec
index 60bedca237a..d9ed99836e3 100644
--- a/src/test/isolation/specs/deadlock-hard.spec
+++ b/src/test/isolation/specs/deadlock-hard.spec
@@ -25,6 +25,8 @@ setup { BEGIN; SET deadlock_timeout = '100s'; }
step s1a1 { LOCK TABLE a1; }
step s1a2 { LOCK TABLE a2; }
step s1c { COMMIT; }
+step s1sl { SELECT deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s1rl { SELECT pg_stat_reset_shared('lock'); }
session s2
setup { BEGIN; SET deadlock_timeout = '100s'; }
@@ -67,6 +69,7 @@ setup { BEGIN; SET deadlock_timeout = '10ms'; }
step s8a8 { LOCK TABLE a8; }
step s8a1 { LOCK TABLE a1; }
step s8c { COMMIT; }
+step s8f { SELECT pg_stat_force_next_flush(); }
# Note: when s8a1 detects the deadlock and fails, s7a8 is released, making
# it timing-dependent which query completion is received first by the tester.
@@ -76,4 +79,4 @@ step s8c { COMMIT; }
# dummy blocking mark to s8a1 to ensure it will be reported as "waiting"
# regardless of that.
-permutation s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s7c s6c s5c s4c s3c s2c s1c
+permutation s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
diff --git a/src/test/regress/expected/advisory_lock.out b/src/test/regress/expected/advisory_lock.out
index 02e07765ac2..fdaa1756ba4 100644
--- a/src/test/regress/expected/advisory_lock.out
+++ b/src/test/regress/expected/advisory_lock.out
@@ -2,6 +2,12 @@
-- ADVISORY LOCKS
--
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+ pg_stat_reset_shared
+----------------------
+
+(1 row)
+
BEGIN;
SELECT
pg_advisory_xact_lock(1), pg_advisory_xact_lock_shared(2),
@@ -48,6 +54,12 @@ WARNING: you don't own a lock of type ShareLock
f | f | f | f
(1 row)
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
@@ -56,6 +68,12 @@ SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid
0
(1 row)
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
+ requests
+----------
+ 4
+(1 row)
+
BEGIN;
-- holding both session and xact locks on the same objects, xact first
SELECT
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 78a37d9fc8f..80999465baf 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1951,6 +1951,13 @@ pg_stat_io| SELECT backend_type,
fsync_time,
stats_reset
FROM pg_stat_get_io() b(backend_type, object, context, reads, read_bytes, read_time, writes, write_bytes, write_time, writebacks, writeback_time, extends, extend_bytes, extend_time, hits, evictions, reuses, fsyncs, fsync_time, stats_reset);
+pg_stat_lock| SELECT locktype,
+ requests,
+ timeouts,
+ deadlock_timeouts,
+ fastpath,
+ stats_reset
+ FROM pg_stat_get_lock() l(locktype, requests, timeouts, deadlock_timeouts, fastpath, stats_reset);
pg_stat_progress_analyze| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/sql/advisory_lock.sql b/src/test/regress/sql/advisory_lock.sql
index 8513ab8e98f..f1bff60fd37 100644
--- a/src/test/regress/sql/advisory_lock.sql
+++ b/src/test/regress/sql/advisory_lock.sql
@@ -4,6 +4,8 @@
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+
BEGIN;
SELECT
@@ -26,12 +28,14 @@ SELECT
pg_advisory_unlock(1), pg_advisory_unlock_shared(2),
pg_advisory_unlock(1, 1), pg_advisory_unlock_shared(2, 2);
+SELECT pg_stat_force_next_flush();
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
BEGIN;
--
2.34.1
^ permalink raw reply [nested|flat] 71+ messages in thread
* Re: Adding locks statistics
2026-02-13 07:36 Re: Adding locks statistics Michael Paquier <[email protected]>
2026-02-13 10:24 ` Re: Adding locks statistics Bertrand Drouvot <[email protected]>
@ 2026-02-13 21:13 ` Andres Freund <[email protected]>
2026-02-16 04:18 ` Re: Adding locks statistics Michael Paquier <[email protected]>
2026-02-16 10:10 ` Re: Adding locks statistics Bertrand Drouvot <[email protected]>
0 siblings, 2 replies; 71+ messages in thread
From: Andres Freund @ 2026-02-13 21:13 UTC (permalink / raw)
To: Bertrand Drouvot <[email protected]>; +Cc: Michael Paquier <[email protected]>; Jeff Davis <[email protected]>; Greg Sabino Mullane <[email protected]>; [email protected]
Hi,
On 2026-02-13 10:24:52 +0000, Bertrand Drouvot wrote:
> On Fri, Feb 13, 2026 at 04:36:57PM +0900, Michael Paquier wrote:
> > On Tue, Feb 10, 2026 at 07:30:50AM +0000, Bertrand Drouvot wrote:
> > > New rebase due to 73d60ac385a.
> > So my suggestion for the moment would be to be more frugal (yeah I
> > know, sorry..) and limit ourselves to four fields: deadlock_timeout,
> > requests, fastpath and timeouts. Three fields to compare with
> > requests, one for each GUC.
>
> That's fine by me. We could still add the others in the future if we feel the
> need. Done that way in the attached.
I'm not sure that it's unproblematic to add multiple pgstat count calls to
every lock acquisition, particularly if it's a fastpath acquisition or a
virtualxid lock. Notably these are external function calls, not just
increments of a counter in an inline function.
I also don't really know what one would do with some of the information?
What does the number of virtualxid lock acquisitions tell you that the numbers
of transactions doesn't already tell you in a more understandable way?
What does it tell you that the deadlock checker ran N times? It notably
doesn't count deadlocks, it counts how often we checked for deadlocks.
The percentage of fastpath locks also seems not really informative, because
that could be because we ran out of space for fastpath locks, or because a
lock mode that's ineligible for fastpath locks was used.
What I would actually count is the amount of time waiting for locks, that
seems vastly more useful than the number of acquisitions. We already do a
GetCurrentTimestamp() inside the timer activations for deadlock timeout, we
probably can figure out a way to reuse that to reduce the increase in overhead
due to timing. We could also just count the wait time after the deadlock
check has run.
> @@ -17,6 +17,7 @@
> #include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
> #include "replication/conflict.h"
> #include "replication/worker_internal.h"
> +#include "storage/lock.h"
> #include "utils/backend_progress.h" /* for backward compatibility */ /* IWYU pragma: export */
> #include "utils/backend_status.h" /* for backward compatibility */ /* IWYU pragma: export */
> #include "utils/pgstat_kind.h"
> @@ -342,6 +343,25 @@ typedef struct PgStat_IO
> PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
> } PgStat_IO;
I don't like the amount of headers this addition will indirectly include in a
lot of places.
I'm also pretty unhappy about an include of access/transam.h recently having
been added. And worker_internal.h quite obviously, given the name, has no
business being included here, and also includes a lot more. Grrr. I'll start
a thread.
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 71+ messages in thread
* Re: Adding locks statistics
2026-02-13 07:36 Re: Adding locks statistics Michael Paquier <[email protected]>
2026-02-13 10:24 ` Re: Adding locks statistics Bertrand Drouvot <[email protected]>
2026-02-13 21:13 ` Re: Adding locks statistics Andres Freund <[email protected]>
@ 2026-02-16 04:18 ` Michael Paquier <[email protected]>
1 sibling, 0 replies; 71+ messages in thread
From: Michael Paquier @ 2026-02-16 04:18 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Bertrand Drouvot <[email protected]>; Jeff Davis <[email protected]>; Greg Sabino Mullane <[email protected]>; [email protected]
On Fri, Feb 13, 2026 at 04:13:39PM -0500, Andres Freund wrote:
> I'm not sure that it's unproblematic to add multiple pgstat count calls to
> every lock acquisition, particularly if it's a fastpath acquisition or a
> virtualxid lock. Notably these are external function calls, not just
> increments of a counter in an inline function.
Right. I am not completely sure if this is really free, either,
particularly for a fastpath lock that we want to be.. Faster.
> What I would actually count is the amount of time waiting for locks, that
> seems vastly more useful than the number of acquisitions. We already do a
> GetCurrentTimestamp() inside the timer activations for deadlock timeout, we
> probably can figure out a way to reuse that to reduce the increase in overhead
> due to timing. We could also just count the wait time after the deadlock
> check has run.
That sounds like an interesting suggestion, yes. That's not going to
be bounded by performance if we know that we are already waiting.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 71+ messages in thread
* Re: Adding locks statistics
2026-02-13 07:36 Re: Adding locks statistics Michael Paquier <[email protected]>
2026-02-13 10:24 ` Re: Adding locks statistics Bertrand Drouvot <[email protected]>
2026-02-13 21:13 ` Re: Adding locks statistics Andres Freund <[email protected]>
@ 2026-02-16 10:10 ` Bertrand Drouvot <[email protected]>
1 sibling, 0 replies; 71+ messages in thread
From: Bertrand Drouvot @ 2026-02-16 10:10 UTC (permalink / raw)
To: Andres Freund <[email protected]>; +Cc: Michael Paquier <[email protected]>; Jeff Davis <[email protected]>; Greg Sabino Mullane <[email protected]>; [email protected]
Hi,
On Fri, Feb 13, 2026 at 04:13:39PM -0500, Andres Freund wrote:
> Hi,
>
> On 2026-02-13 10:24:52 +0000, Bertrand Drouvot wrote:
> >
> > That's fine by me. We could still add the others in the future if we feel the
> > need. Done that way in the attached.
>
> I'm not sure that it's unproblematic to add multiple pgstat count calls to
> every lock acquisition, particularly if it's a fastpath acquisition or a
> virtualxid lock. Notably these are external function calls, not just
> increments of a counter in an inline function.
Yeah, we could create inline functions instead.
> I also don't really know what one would do with some of the information?
>
> What does the number of virtualxid lock acquisitions tell you that the numbers
> of transactions doesn't already tell you in a more understandable way?
I agree not that much, except being able to compute a transactions/virtualxid
ratio or such. Also the idea was to report the same as pg_locks so that one
could start sampling pg_locks if he needs more details.
> What does it tell you that the deadlock checker ran N times? It notably
> doesn't count deadlocks, it counts how often we checked for deadlocks.
>
> The percentage of fastpath locks also seems not really informative, because
> that could be because we ran out of space for fastpath locks, or because a
> lock mode that's ineligible for fastpath locks was used.
Right, maybe we could add an "exceed fastpath slots" or such counter?
> What I would actually count is the amount of time waiting for locks, that
> seems vastly more useful than the number of acquisitions. We already do a
> GetCurrentTimestamp() inside the timer activations for deadlock timeout, we
> probably can figure out a way to reuse that to reduce the increase in overhead
> due to timing. We could also just count the wait time after the deadlock
> check has run.
Yeah, providing the wait_time would be great. Just to be sure, are you suggesting
to remove all the fields (i.e requests, timeouts, deadlock_timeouts and fastpath)
and just add a wait_time field instead? I think that keeping requests would make
sense to be able to get the average wait time per request.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 71+ messages in thread
end of thread, other threads:[~2026-02-16 10:10 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]>
2026-02-13 07:36 Re: Adding locks statistics Michael Paquier <[email protected]>
2026-02-13 10:24 ` Re: Adding locks statistics Bertrand Drouvot <[email protected]>
2026-02-13 21:13 ` Re: Adding locks statistics Andres Freund <[email protected]>
2026-02-16 04:18 ` Re: Adding locks statistics Michael Paquier <[email protected]>
2026-02-16 10:10 ` Re: Adding locks statistics Bertrand Drouvot <[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