($INBOX_DIR/description missing)
help / color / mirror / Atom feed[PATCH 7/9] Remove the special batch mode, use a larger buffer always
74+ messages / 3 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ 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; 74+ 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] 74+ messages in thread
* Re: Refactoring SSL tests
@ 2022-02-02 16:09 Andrew Dunstan <[email protected]>
0 siblings, 1 reply; 74+ messages in thread
From: Andrew Dunstan @ 2022-02-02 16:09 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; PostgreSQL Hackers <[email protected]>
On 2/2/22 08:26, Daniel Gustafsson wrote:
> Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
> I'm happy to take pointers on how to improve that.
It feels a bit odd to me from a perl POV. I think it needs to more along
the lines of standard OO patterns. I'll take a stab at that based on
this, might be a few days.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 74+ messages in thread
* Re: Refactoring SSL tests
@ 2022-02-02 19:50 Daniel Gustafsson <[email protected]>
parent: Andrew Dunstan <[email protected]>
0 siblings, 1 reply; 74+ messages in thread
From: Daniel Gustafsson @ 2022-02-02 19:50 UTC (permalink / raw)
To: Andrew Dunstan <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
> On 2 Feb 2022, at 17:09, Andrew Dunstan <[email protected]> wrote:
> On 2/2/22 08:26, Daniel Gustafsson wrote:
>> Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
>> I'm happy to take pointers on how to improve that.
>
> It feels a bit odd to me from a perl POV. I think it needs to more along
> the lines of standard OO patterns. I'll take a stab at that based on
> this, might be a few days.
That would be great, thanks!
--
Daniel Gustafsson https://vmware.com/
^ permalink raw reply [nested|flat] 74+ messages in thread
* Re: Refactoring SSL tests
@ 2022-02-07 16:29 Andrew Dunstan <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 74+ messages in thread
From: Andrew Dunstan @ 2022-02-07 16:29 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On 2/2/22 14:50, Daniel Gustafsson wrote:
>> On 2 Feb 2022, at 17:09, Andrew Dunstan <[email protected]> wrote:
>> On 2/2/22 08:26, Daniel Gustafsson wrote:
>>> Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
>>> I'm happy to take pointers on how to improve that.
>> It feels a bit odd to me from a perl POV. I think it needs to more along
>> the lines of standard OO patterns. I'll take a stab at that based on
>> this, might be a few days.
> That would be great, thanks!
>
Here's the result of that surgery. It's a little incomplete in that it
needs some POD adjustment, but I think the code is right - it passes
testing for me.
One of the advantages of this, apart from being more idiomatic, is that
by avoiding the use of package level variables you can have two
SSL::Server objects, one for OpenSSL and (eventually) one for NSS. This
was the original motivation for the recent install_path additions to
PostgreSQL::Test::Cluster, so it complements that work nicely.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
Attachments:
[text/x-patch] 0001-ssl-test-refactoring.patch (43.7K, ../../[email protected]/2-0001-ssl-test-refactoring.patch)
download | inline diff:
From fa9bd034210aae4e11eed463e1a1365231c944f5 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <[email protected]>
Date: Mon, 7 Feb 2022 11:04:53 -0500
Subject: [PATCH] ssl test refactoring
---
src/test/ssl/t/001_ssltests.pl | 144 +++++------
src/test/ssl/t/002_scram.pl | 21 +-
src/test/ssl/t/003_sslinfo.pl | 33 ++-
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 227 ++++++++++++++++++
src/test/ssl/t/SSL/Server.pm | 332 ++++++++++++++++++++++++++
src/test/ssl/t/SSLServer.pm | 219 -----------------
6 files changed, 647 insertions(+), 329 deletions(-)
create mode 100644 src/test/ssl/t/SSL/Backend/OpenSSL.pm
create mode 100644 src/test/ssl/t/SSL/Server.pm
delete mode 100644 src/test/ssl/t/SSLServer.pm
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1fb15ce80..d5383a58ce 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -8,20 +8,24 @@ use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
-use File::Copy;
-
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
-else
+
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
{
- plan tests => 110;
+ $ssl_server->switch_server_cert(@_);
}
#### Some configuration
@@ -36,39 +40,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-# The client's private key must not be world-readable, so take a copy
-# of the key stored in the code tree and update its permissions.
-#
-# This changes to using keys stored in a temporary path for the rest of
-# the tests. To get the full path for inclusion in connection strings, the
-# %key hash can be interrogated.
-my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
-my %key;
-my @keys = (
- "client.key", "client-revoked.key",
- "client-der.key", "client-encrypted-pem.key",
- "client-encrypted-der.key", "client-dn.key");
-foreach my $keyfile (@keys)
-{
- copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
- or die
- "couldn't copy ssl/$keyfile to $cert_tempdir/$keyfile for permissions change: $!";
- chmod 0600, "$cert_tempdir/$keyfile"
- or die "failed to change permissions on $cert_tempdir/$keyfile: $!";
- $key{$keyfile} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
- $key{$keyfile} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
-}
-
-# Also make a copy of that explicitly world-readable. We can't
-# necessarily rely on the file in the source tree having those
-# permissions.
-copy("ssl/client.key", "$cert_tempdir/client_wrongperms.key")
- or die
- "couldn't copy ssl/client_key to $cert_tempdir/client_wrongperms.key for permission change: $!";
-chmod 0644, "$cert_tempdir/client_wrongperms.key"
- or die "failed to change permissions on $cert_tempdir/client_wrongperms.key: $!";
-$key{'client_wrongperms.key'} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
-$key{'client_wrongperms.key'} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
#### Set up the server.
note "setting up data directory";
@@ -83,31 +54,31 @@ $node->start;
# Run this before we lock down access below.
my $result = $node->safe_psql('postgres', "SHOW ssl_library");
-is($result, 'OpenSSL', 'ssl_library parameter');
+is($result, $ssl_server->ssl_library(), 'ssl_library parameter');
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
- 'trust');
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
note "testing password-protected keys";
-open my $sslconf, '>', $node->data_dir . "/sslconfig.conf";
-print $sslconf "ssl=on\n";
-print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
-print $sslconf "ssl_key_file='server-password.key'\n";
-print $sslconf "ssl_passphrase_command='echo wrongpassword'\n";
-close $sslconf;
+switch_server_cert($node,
+ certfile => 'server-cn-only',
+ cafile => 'root+client_ca',
+ keyfile => 'server-password',
+ passphrase_cmd => 'echo wrongpassword',
+ restart => 'no' );
command_fails(
[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
'restart fails with password-protected key file with wrong password');
$node->_update_pid(0);
-open $sslconf, '>', $node->data_dir . "/sslconfig.conf";
-print $sslconf "ssl=on\n";
-print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
-print $sslconf "ssl_key_file='server-password.key'\n";
-print $sslconf "ssl_passphrase_command='echo secret1'\n";
-close $sslconf;
+switch_server_cert($node,
+ certfile => 'server-cn-only',
+ cafile => 'root+client_ca',
+ keyfile => 'server-password',
+ passphrase_cmd => 'echo secret1',
+ restart => 'no');
command_ok(
[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
@@ -140,7 +111,7 @@ command_ok(
note "running client tests";
-switch_server_cert($node, 'server-cn-only');
+switch_server_cert($node, certfile => 'server-cn-only');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -254,7 +225,7 @@ $node->connect_fails(
);
# Test Subject Alternative Names.
-switch_server_cert($node, 'server-multiple-alt-names');
+switch_server_cert($node, certfile => 'server-multiple-alt-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -283,7 +254,7 @@ $node->connect_fails(
# Test certificate with a single Subject Alternative Name. (this gives a
# slightly different error message, that's all)
-switch_server_cert($node, 'server-single-alt-name');
+switch_server_cert($node, certfile => 'server-single-alt-name');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -307,7 +278,7 @@ $node->connect_fails(
# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
# should be ignored when the certificate has both.
-switch_server_cert($node, 'server-cn-and-alt-names');
+switch_server_cert($node, certfile => 'server-cn-and-alt-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -325,7 +296,7 @@ $node->connect_fails(
# Finally, test a server certificate that has no CN or SANs. Of course, that's
# not a very sensible certificate, but libpq should handle it gracefully.
-switch_server_cert($node, 'server-no-names');
+switch_server_cert($node, certfile => 'server-no-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
@@ -340,7 +311,7 @@ $node->connect_fails(
qr/could not get server's host name from server certificate/);
# Test that the CRL works
-switch_server_cert($node, 'server-revoked');
+switch_server_cert($node, certfile => 'server-revoked');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -406,34 +377,34 @@ $node->connect_fails(
# correct client cert in unencrypted PEM
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
"certificate authorization succeeds with correct client cert in PEM format"
);
# correct client cert in unencrypted DER
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-der.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-der.key'),
"certificate authorization succeeds with correct client cert in DER format"
);
# correct client cert in encrypted PEM
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword='dUmmyP^#+'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword='dUmmyP^#+'",
"certificate authorization succeeds with correct client cert in encrypted PEM format"
);
# correct client cert in encrypted DER
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-der.key'} sslpassword='dUmmyP^#+'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-der.key') . " sslpassword='dUmmyP^#+'",
"certificate authorization succeeds with correct client cert in encrypted DER format"
);
# correct client cert in encrypted PEM with wrong password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword='wrong'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword='wrong'",
"certificate authorization fails with correct client cert and wrong password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": bad decrypt\E!
+ qr!private key file \".*client-encrypted-pem\.key\": bad decrypt!,
);
@@ -441,7 +412,7 @@ $node->connect_fails(
my $dn_connstr = "$common_connstr dbname=certdb_dn";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with DN mapping",
log_like => [
qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/
@@ -451,14 +422,14 @@ $node->connect_ok(
$dn_connstr = "$common_connstr dbname=certdb_dn_re";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with DN regex mapping");
# same thing but using explicit CN
$dn_connstr = "$common_connstr dbname=certdb_cn";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with CN mapping",
# the full DN should still be used as the authenticated identity
log_like => [
@@ -476,18 +447,18 @@ TODO:
# correct client cert in encrypted PEM with empty password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword=''",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword=''",
"certificate authorization fails with correct client cert and empty password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": processing error\E!
+ qr!private key file \".*client-encrypted-pem\.key\": processing error!
);
# correct client cert in encrypted PEM with no password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key'),
"certificate authorization fails with correct client cert and no password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": processing error\E!
+ qr!private key file \".*client-encrypted-pem\.key\": processing error!
);
}
@@ -530,12 +501,12 @@ command_like(
'-P',
'null=_null_',
'-d',
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
'-c',
"SELECT * FROM pg_stat_ssl WHERE pid = pg_backend_pid()"
],
qr{^pid,ssl,version,cipher,bits,client_dn,client_serial,issuer_dn\r?\n
- ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,/CN=ssltestuser,$serialno,\Q/CN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx,
+ ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,/?CN=ssltestuser,$serialno,/?\QCN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx,
'pg_stat_ssl with client certificate');
# client key with wrong permissions
@@ -544,16 +515,16 @@ SKIP:
skip "Permissions check not enforced on Windows", 2 if ($windows_os);
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client_wrongperms.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client_wrongperms.key'),
"certificate authorization fails because of file permissions",
expected_stderr =>
- qr!\Qprivate key file "$key{'client_wrongperms.key'}" has group or world access\E!
+ qr!private key file \".*client_wrongperms\.key\" has group or world access!
);
}
# client cert belonging to another user
$node->connect_fails(
- "$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=anotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"certificate authorization fails with client cert belonging to another user",
expected_stderr =>
qr/certificate authentication failed for user "anotheruser"/,
@@ -563,7 +534,7 @@ $node->connect_fails(
# revoked client cert
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=$key{'client-revoked.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'),
"certificate authorization fails with revoked client cert",
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
# revoked certificates should not authenticate the user
@@ -576,13 +547,13 @@ $common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-full succeeds with matching username and Common Name",
# verify-full does not provide authentication
log_unlike => [qr/connection authenticated:/],);
$node->connect_fails(
- "$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=anotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-full fails with mismatching username and Common Name",
expected_stderr =>
qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
@@ -592,15 +563,15 @@ $node->connect_fails(
# Check that connecting with auth-optionverify-ca in pg_hba :
# works, when username doesn't match Common Name
$node->connect_ok(
- "$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=yetanotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
# verify-full does not provide authentication
log_unlike => [qr/connection authenticated:/],);
# intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
-switch_server_cert($node, 'server-cn-only', 'root_ca');
+switch_server_cert($node, certfile => 'server-cn-only', cafile => 'root_ca');
$common_connstr =
- "user=ssltestuser dbname=certdb sslkey=$key{'client.key'} sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
+ "user=ssltestuser dbname=certdb " . sslkey('client.key') . " sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
$node->connect_ok(
"$common_connstr sslmode=require sslcert=ssl/client+client_ca.crt",
@@ -611,11 +582,12 @@ $node->connect_fails(
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/);
# test server-side CRL directory
-switch_server_cert($node, 'server-cn-only', undef, undef,
- 'root+client-crldir');
+switch_server_cert($node, certfile => 'server-cn-only', crldir => 'root+client-crldir');
# revoked client cert
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=$key{'client-revoked.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'),
"certificate authorization fails with revoked client cert with server-side CRL directory",
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
+
+done_testing();
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 86312be88c..bd7a84cec2 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -14,13 +14,24 @@ use File::Copy;
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
+{
+ $ssl_server->switch_server_cert(@_);
+}
+
+
# This is the hostname used to connect to the server.
my $SERVERHOSTADDR = '127.0.0.1';
# This is the pattern to use in pg_hba.conf to match incoming connections.
@@ -30,8 +41,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
my $supports_tls_server_end_point =
check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
-my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
-
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
@@ -48,9 +57,9 @@ $ENV{PGPORT} = $node->port;
$node->start;
# Configure server for SSL connections, with password handling.
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
"scram-sha-256", 'password' => "pass", 'password_enc' => "scram-sha-256");
-switch_server_cert($node, 'server-cn-only');
+switch_server_cert($node, certfile => 'server-cn-only');
$ENV{PGPASSWORD} = "pass";
$common_connstr =
"dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
@@ -118,4 +127,4 @@ $node->connect_ok(
qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
]);
-done_testing($number_of_tests);
+done_testing();
diff --git a/src/test/ssl/t/003_sslinfo.pl b/src/test/ssl/t/003_sslinfo.pl
index 8c760b39db..bc555048da 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -12,18 +12,24 @@ use File::Copy;
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
-else
+
+#### Some configuration
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
{
- plan tests => 13;
+ $ssl_server->switch_server_cert(@_);
}
-#### Some configuration
# This is the hostname used to connect to the server. This cannot be a
# hostname, because the server certificate is always for the domain
@@ -35,17 +41,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-# The client's private key must not be world-readable, so take a copy
-# of the key stored in the code tree and update its permissions.
-my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
-my $client_tmp_key = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_ext.key");
-copy("ssl/client_ext.key", "$cert_tempdir/client_ext.key")
- or die
- "couldn't copy ssl/client_ext.key to $cert_tempdir/client_ext.key for permissions change: $!";
-chmod 0600, "$cert_tempdir/client_ext.key"
- or die "failed to change permissions on $cert_tempdir/client_ext.key: $!";
-$client_tmp_key =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
-
#### Set up the server.
note "setting up data directory";
@@ -58,17 +53,17 @@ $ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
'trust', extensions => [ qw(sslinfo) ]);
# We aren't using any CRL's in this suite so we can keep using server-revoked
# as server certificate for simple client.crt connection much like how the
# 001 test does.
-switch_server_cert($node, 'server-revoked');
+switch_server_cert($node, certfile => 'server-revoked');
$common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR " .
- "user=ssltestuser sslcert=ssl/client_ext.crt sslkey=$client_tmp_key";
+ "user=ssltestuser sslcert=ssl/client_ext.crt " . sslkey('client_ext.key');
# Make sure we can connect even though previous test suites have established this
$node->connect_ok(
@@ -135,3 +130,5 @@ $result = $node->safe_psql("certdb",
"SELECT value, critical FROM ssl_extension_info() WHERE name = 'basicConstraints';",
connstr => $common_connstr);
is($result, 'CA:FALSE|t', 'extract extension from cert');
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
new file mode 100644
index 0000000000..01b8a0857e
--- /dev/null
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -0,0 +1,227 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Backend::OpenSSL
+
+=head1 SYNOPSIS
+
+ use SSL::Backend::OpenSSL;
+
+ my $backend = SSL::backend::OpenSSL->new();
+
+ $backend->init($pgdata);
+
+=head1 DESCRIPTION
+
+SSL::Backend::OpenSSL implements the library specific parts in SSL::Server
+for a PostgreSQL cluster compiled against OpenSSL.
+
+=cut
+
+package SSL::Backend::OpenSSL;
+
+use strict;
+use warnings;
+use File::Basename;
+use File::Copy;
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Backend::OpenSSL::get_new_openssl_backend()
+
+Create a new instance of the OpenSSL backend.
+
+=cut
+
+sub new
+{
+ my ($class) = @_;
+
+ my $self = { _library => 'OpenSSL', key => {} };
+
+ bless $self, $class;
+
+ return $self;
+}
+
+=pod
+
+=item $backend->init()
+
+Install certificates, keys and CRL files required to run the tests against an
+OpenSSL backend.
+
+=cut
+
+sub init
+{
+ my ($self, $pgdata) = @_;
+
+ # Install server certificates and keys into the cluster data directory.
+ _copy_files("ssl/server-*.crt", $pgdata);
+ _copy_files("ssl/server-*.key", $pgdata);
+ chmod(0600, glob "$pgdata/server-*.key")
+ or die "failed to change permissions on server keys: $!";
+ _copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root_ca.crt", $pgdata);
+ _copy_files("ssl/root+client.crl", $pgdata);
+ mkdir("$pgdata/root+client-crldir")
+ or die "unable to create server CRL dir $pgdata/root+client-crldir: $!";
+ _copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
+
+ # The client's private key must not be world-readable, so take a copy
+ # of the key stored in the code tree and update its permissions.
+ #
+ # This changes to using keys stored in a temporary path for the rest of
+ # the tests. To get the full path for inclusion in connection strings, the
+ # %key hash can be interrogated.
+ my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
+ my @keys = (
+ "client.key", "client-revoked.key",
+ "client-der.key", "client-encrypted-pem.key",
+ "client-encrypted-der.key", "client-dn.key",
+ "client_ext.key");
+ foreach my $keyfile (@keys)
+ {
+ copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
+ or die
+ "couldn't copy ssl/$keyfile to $cert_tempdir/$keyfile for permissions change: $!";
+ chmod 0600, "$cert_tempdir/$keyfile"
+ or die "failed to change permissions on $cert_tempdir/$keyfile: $!";
+ $self->{key}->{$keyfile} =
+ PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
+ $self->{key}->{$keyfile} =~ s!\\!/!g
+ if $PostgreSQL::Test::Utils::windows_os;
+ }
+
+ # Also make a copy of client.key explicitly world-readable in order to be
+ # able to test incorrect permissions. We can't necessarily rely on the
+ # file in the source tree having those permissions.
+ copy("ssl/client.key", "$cert_tempdir/client_wrongperms.key")
+ or die
+ "couldn't copy ssl/client_key to $cert_tempdir/client_wrongperms.key for permission change: $!";
+ chmod 0644, "$cert_tempdir/client_wrongperms.key"
+ or die "failed to change permissions on $cert_tempdir/client_wrongperms.key: $!";
+ $self->{key}->{'client_wrongperms.key'} =
+ PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
+ $self->key->{'client_wrongperms.key'} =~ s!\\!/!g
+ if $PostgreSQL::Test::Utils::windows_os;
+}
+
+=pod
+
+=item $backend->get_sslkey()
+
+Get an 'sslkey' connection string parameter for the specified key which has
+the correct path for direct inclusion in a connection string.
+
+=cut
+
+sub get_sslkey
+{
+ my ($self, $keyfile) = @_;
+
+ return " sslkey=$self->{key}->{$keyfile}";
+}
+
+=pod
+
+=item $backend->set_server_cert(params)
+
+Change the configuration to use given server cert, key and crl file(s).
+
+=over
+
+=item cafile => B<value>
+
+The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
+default to 'root+client_ca.crt'.
+
+=item certfile => B<value>
+
+The server certificate file to use for the C<ssl_cert_file> GUC.
+
+=item keyfile => B<value>
+
+The private key file to use for the C<ssl_key_file GUC>. If omitted it will
+default to the B<certfile>.key.
+
+=item crlfile => B<value>
+
+The CRL file to use for the C<ssl_crl_file> GUC. If omitted it will default to
+'root+client.crl'.
+
+=item crldir => B<value>
+
+The CRL directory to use for the C<ssl_crl_dir> GUC. If omitted,
+C<no ssl_crl_dir> configuration parameter will be set.
+
+=back
+
+=cut
+
+sub set_server_cert
+{
+ my ($self, $params) = @_;
+
+ $params->{cafile} = 'root+client_ca' unless defined $params->{cafile};
+ $params->{crlfile} = 'root+client.crl' unless defined $params->{crlfile};
+ $params->{keyfile} = $params->{certfile} unless defined $params->{keyfile};
+
+ my $sslconf =
+ "ssl_ca_file='$params->{cafile}.crt'\n"
+ . "ssl_cert_file='$params->{certfile}.crt'\n"
+ . "ssl_key_file='$params->{keyfile}.key'\n"
+ . "ssl_crl_file='$params->{crlfile}'\n";
+ $sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
+ if defined $params->{crldir};
+
+ return $sslconf;
+}
+
+=pod
+
+=item $backend->get_library()
+
+Returns the name of the SSL library, in this case "OpenSSL".
+
+=cut
+
+sub get_library
+{
+ my ($self) = @_;
+
+ return $self->{_library};
+}
+
+# Internal method for copying a set of files, taking into account wildcards
+sub _copy_files
+{
+ my $orig = shift;
+ my $dest = shift;
+
+ my @orig_files = glob $orig;
+ foreach my $orig_file (@orig_files)
+ {
+ my $base_file = basename($orig_file);
+ copy($orig_file, "$dest/$base_file")
+ or die "Could not copy $orig_file to $dest";
+ }
+ return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
new file mode 100644
index 0000000000..fd6ce924f2
--- /dev/null
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -0,0 +1,332 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Server - Class for setting up SSL in a PostgreSQL Cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use SSL::Server;
+
+ # Create a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+
+ # Initialize and start the new cluster
+ $node->init;
+ $node->start;
+
+ # Configure SSL on the newly formed cluster
+ configure_test_server_for_ssl($node, '127.0.0.1', '127.0.0.1/32', 'trust');
+
+=head1 DESCRIPTION
+
+SSL::Server configures an existing test cluster, for the SSL regression tests.
+
+The server is configured as follows:
+
+=over
+
+=item * SSL enabled, with the server certificate specified by arguments to switch_server_cert function.
+
+=item * reject non-SSL connections
+
+=item * a database called trustdb that lets anyone in
+
+=item * another database called certdb that uses certificate authentication, ie. the client must present a valid certificate signed by the client CA
+
+=back
+
+The server is configured to only accept connections from localhost. If you
+want to run the client from another host, you'll have to configure that
+manually.
+
+Note: Someone running these test could have key or certificate files in their
+~/.postgresql/, which would interfere with the tests. The way to override that
+is to specify sslcert=invalid and/or sslrootcert=invalid if no actual
+certificate is used for a particular test. libpq will ignore specifications
+that name nonexisting files. (sslkey and sslcrl do not need to specified
+explicitly because an invalid sslcert or sslrootcert, respectively, causes
+those to be ignored.)
+
+The SSL::Server module presents a SSL library abstraction to the test writer,
+which in turn use modules in SSL::Backend which implements the SSL library
+specific infrastructure. Currently only OpenSSL is supported.
+
+=cut
+
+package SSL::Server;
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use SSL::Backend::OpenSSL qw(get_new_openssl_backend);
+
+sub new
+{
+ my $class = shift;
+ my $flavor = shift || $ENV{with_ssl};
+ die "SSL flavor not defined" unless $flavor;
+ my $self = {};
+ bless $self, $class;
+ if ($flavor =~ /\Aopenssl\z/i)
+ {
+ $self->{flavor} = 'openssl';
+ $self->{backend} = SSL::Backend::OpenSSL->new();
+ }
+ else
+ {
+ die "SSL flavor $flavor unknown";
+ }
+ return $self;
+}
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item sslkey(filename)
+
+Return a C<sslkey> construct for the specified key for use in a connection
+string.
+
+=cut
+
+sub sslkey
+{
+ my $self = shift;
+ my $keyfile = shift;
+ my $backend = $self->{backend};
+
+ return $backend->get_sslkey($keyfile);
+}
+
+=pod
+
+=item configure_test_server_for_ssl()
+
+Configure the cluster for listening on SSL connections.
+
+=cut
+
+# serverhost: what to put in listen_addresses, e.g. '127.0.0.1'
+# servercidr: what to put in pg_hba.conf, e.g. '127.0.0.1/32'
+sub configure_test_server_for_ssl
+{
+ my $self=shift;
+ my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
+ my $backend = $self->{backend};
+ my $pgdata = $node->data_dir;
+
+ my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
+
+ # Create test users and databases
+ $node->psql('postgres', "CREATE USER ssltestuser");
+ $node->psql('postgres', "CREATE USER md5testuser");
+ $node->psql('postgres', "CREATE USER anotheruser");
+ $node->psql('postgres', "CREATE USER yetanotheruser");
+
+ foreach my $db (@databases)
+ {
+ $node->psql('postgres', "CREATE DATABASE $db");
+ }
+
+ # Update password of each user as needed.
+ if (defined($params{password}))
+ {
+ die "Password encryption must be specified when password is set"
+ unless defined($params{password_enc});
+
+ $node->psql('postgres',
+ "SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
+ );
+ # A special user that always has an md5-encrypted password
+ $node->psql('postgres',
+ "SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
+ );
+ $node->psql('postgres',
+ "SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
+ );
+ }
+
+ # Create any extensions requested in the setup
+ if (defined($params{extensions}))
+ {
+ foreach my $extension (@{$params{extensions}})
+ {
+ foreach my $db (@databases)
+ {
+ $node->psql($db, "CREATE EXTENSION $extension CASCADE;");
+ }
+ }
+ }
+
+ # enable logging etc.
+ open my $conf, '>>', "$pgdata/postgresql.conf";
+ print $conf "fsync=off\n";
+ print $conf "log_connections=on\n";
+ print $conf "log_hostname=on\n";
+ print $conf "listen_addresses='$serverhost'\n";
+ print $conf "log_statement=all\n";
+
+ # enable SSL and set up server key
+ print $conf "include 'sslconfig.conf'\n";
+
+ close $conf;
+
+ # SSL configuration will be placed here
+ open my $sslconf, '>', "$pgdata/sslconfig.conf";
+ close $sslconf;
+
+ # Perform backend specific configuration
+ $backend->init($pgdata);
+
+ # Stop and restart server to load new listen_addresses.
+ $node->restart;
+
+ # Change pg_hba after restart because hostssl requires ssl=on
+ _configure_hba_for_ssl($node, $servercidr, $authmethod);
+
+ return;
+}
+
+=pod
+
+=item SSL::Server::ssl_library()
+
+Get the name of the currently used SSL backend.
+
+=cut
+
+sub ssl_library
+{
+ my $self = shift;
+ my $backend = $self->{backend};
+
+ return $backend->get_library();
+}
+
+=pod
+
+=item switch_server_cert()
+
+Change the configuration to use the given set of certificate, key, ca and
+CRL, and potentially reload the configuration by restarting the server so
+that the configuration takes effect. Restarting is the default, passing
+restart => 'no' opts out of it leaving the server running.
+
+=over
+
+=item cafile => B<value>
+
+The CA certificate to use. Implementation is SSL backend specific.
+
+=item certfile => B<value>
+
+The certificate file to use. Implementation is SSL backend specific.
+
+=item keyfile => B<value>
+
+The private key to to use. Implementation is SSL backend specific.
+
+=item crlfile => B<value>
+
+The CRL file to use. Implementation is SSL backend specific.
+
+=item crldir => B<value>
+
+The CRL directory to use. Implementation is SSL backend specific.
+
+=item passphrase_cmd => B<value>
+
+The passphrase command to use. If not set, an empty passphrase command will
+be set.
+
+=item restart => B<value>
+
+If set to 'no', the server won't be restarted after updating the settings.
+If omitted, or any other value is passed, the server will be restarted before
+returning.
+
+=back
+
+=cut
+
+sub switch_server_cert
+{
+ my $self = shift;
+ my $node = shift;
+ my $backend = $self->{backend};
+ my %params = @_;
+ my $pgdata = $node->data_dir;
+
+ open my $sslconf, '>', "$pgdata/sslconfig.conf";
+ print $sslconf "ssl=on\n";
+ print $sslconf $backend->set_server_cert(\%params);
+ print $sslconf "ssl_passphrase_command='" . $params{passphrase_cmd} . "'\n"
+ if defined $params{passphrase_cmd};
+ close $sslconf;
+
+ return if (defined($params{restart}) && $params{restart} eq 'no');
+
+ $node->restart;
+ return;
+}
+
+
+# Internal function for configuring pg_hba.conf for SSL connections.
+sub _configure_hba_for_ssl
+{
+ my ($node, $servercidr, $authmethod) = @_;
+ my $pgdata = $node->data_dir;
+
+ # Only accept SSL connections from $servercidr. Our tests don't depend on this
+ # but seems best to keep it as narrow as possible for security reasons.
+ #
+ # When connecting to certdb, also check the client certificate.
+ open my $hba, '>', "$pgdata/pg_hba.conf";
+ print $hba
+ "# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n";
+ print $hba
+ "hostssl trustdb md5testuser $servercidr md5\n";
+ print $hba
+ "hostssl trustdb all $servercidr $authmethod\n";
+ print $hba
+ "hostssl verifydb ssltestuser $servercidr $authmethod clientcert=verify-full\n";
+ print $hba
+ "hostssl verifydb anotheruser $servercidr $authmethod clientcert=verify-full\n";
+ print $hba
+ "hostssl verifydb yetanotheruser $servercidr $authmethod clientcert=verify-ca\n";
+ print $hba
+ "hostssl certdb all $servercidr cert\n";
+ print $hba
+ "hostssl certdb_dn all $servercidr cert clientname=DN map=dn\n",
+ "hostssl certdb_dn_re all $servercidr cert clientname=DN map=dnre\n",
+ "hostssl certdb_cn all $servercidr cert clientname=CN map=cn\n";
+ close $hba;
+
+ # Also set the ident maps. Note: fields with commas must be quoted
+ open my $map, ">", "$pgdata/pg_ident.conf";
+ print $map
+ "# MAPNAME SYSTEM-USERNAME PG-USERNAME\n",
+ "dn \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\" ssltestuser\n",
+ "dnre \"/^.*OU=Testing,.*\$\" ssltestuser\n",
+ "cn ssltestuser-dn ssltestuser\n";
+
+ return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm
deleted file mode 100644
index c85c6fd997..0000000000
--- a/src/test/ssl/t/SSLServer.pm
+++ /dev/null
@@ -1,219 +0,0 @@
-
-# Copyright (c) 2021-2022, PostgreSQL Global Development Group
-
-# This module sets up a test server, for the SSL regression tests.
-#
-# The server is configured as follows:
-#
-# - SSL enabled, with the server certificate specified by argument to
-# switch_server_cert function.
-# - ssl/root+client_ca.crt as the CA root for validating client certs.
-# - reject non-SSL connections
-# - a database called trustdb that lets anyone in
-# - another database called certdb that uses certificate authentication, ie.
-# the client must present a valid certificate signed by the client CA
-#
-# The server is configured to only accept connections from localhost. If you
-# want to run the client from another host, you'll have to configure that
-# manually.
-#
-# Note: Someone running these test could have key or certificate files
-# in their ~/.postgresql/, which would interfere with the tests. The
-# way to override that is to specify sslcert=invalid and/or
-# sslrootcert=invalid if no actual certificate is used for a
-# particular test. libpq will ignore specifications that name
-# nonexisting files. (sslkey and sslcrl do not need to specified
-# explicitly because an invalid sslcert or sslrootcert, respectively,
-# causes those to be ignored.)
-
-package SSLServer;
-
-use strict;
-use warnings;
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use File::Basename;
-use File::Copy;
-use Test::More;
-
-use Exporter 'import';
-our @EXPORT = qw(
- configure_test_server_for_ssl
- switch_server_cert
-);
-
-# Copy a set of files, taking into account wildcards
-sub copy_files
-{
- my $orig = shift;
- my $dest = shift;
-
- my @orig_files = glob $orig;
- foreach my $orig_file (@orig_files)
- {
- my $base_file = basename($orig_file);
- copy($orig_file, "$dest/$base_file")
- or die "Could not copy $orig_file to $dest";
- }
- return;
-}
-
-# serverhost: what to put in listen_addresses, e.g. '127.0.0.1'
-# servercidr: what to put in pg_hba.conf, e.g. '127.0.0.1/32'
-sub configure_test_server_for_ssl
-{
- my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
- my $pgdata = $node->data_dir;
-
- my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
-
- # Create test users and databases
- $node->psql('postgres', "CREATE USER ssltestuser");
- $node->psql('postgres', "CREATE USER md5testuser");
- $node->psql('postgres', "CREATE USER anotheruser");
- $node->psql('postgres', "CREATE USER yetanotheruser");
-
- foreach my $db (@databases)
- {
- $node->psql('postgres', "CREATE DATABASE $db");
- }
-
- # Update password of each user as needed.
- if (defined($params{password}))
- {
- die "Password encryption must be specified when password is set"
- unless defined($params{password_enc});
-
- $node->psql('postgres',
- "SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
- );
- # A special user that always has an md5-encrypted password
- $node->psql('postgres',
- "SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
- );
- $node->psql('postgres',
- "SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
- );
- }
-
- # Create any extensions requested in the setup
- if (defined($params{extensions}))
- {
- foreach my $extension (@{$params{extensions}})
- {
- foreach my $db (@databases)
- {
- $node->psql($db, "CREATE EXTENSION $extension CASCADE;");
- }
- }
- }
-
- # enable logging etc.
- open my $conf, '>>', "$pgdata/postgresql.conf";
- print $conf "fsync=off\n";
- print $conf "log_connections=on\n";
- print $conf "log_hostname=on\n";
- print $conf "listen_addresses='$serverhost'\n";
- print $conf "log_statement=all\n";
-
- # enable SSL and set up server key
- print $conf "include 'sslconfig.conf'\n";
-
- close $conf;
-
- # ssl configuration will be placed here
- open my $sslconf, '>', "$pgdata/sslconfig.conf";
- close $sslconf;
-
- # Copy all server certificates and keys, and client root cert, to the data dir
- copy_files("ssl/server-*.crt", $pgdata);
- copy_files("ssl/server-*.key", $pgdata);
- chmod(0600, glob "$pgdata/server-*.key") or die $!;
- copy_files("ssl/root+client_ca.crt", $pgdata);
- copy_files("ssl/root_ca.crt", $pgdata);
- copy_files("ssl/root+client.crl", $pgdata);
- mkdir("$pgdata/root+client-crldir");
- copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
-
- # Stop and restart server to load new listen_addresses.
- $node->restart;
-
- # Change pg_hba after restart because hostssl requires ssl=on
- configure_hba_for_ssl($node, $servercidr, $authmethod);
-
- return;
-}
-
-# Change the configuration to use given server cert file, and reload
-# the server so that the configuration takes effect.
-sub switch_server_cert
-{
- my $node = $_[0];
- my $certfile = $_[1];
- my $cafile = $_[2] || "root+client_ca";
- my $crlfile = "root+client.crl";
- my $crldir;
- my $pgdata = $node->data_dir;
-
- # defaults to use crl file
- if (defined $_[3] || defined $_[4])
- {
- $crlfile = $_[3];
- $crldir = $_[4];
- }
-
- open my $sslconf, '>', "$pgdata/sslconfig.conf";
- print $sslconf "ssl=on\n";
- print $sslconf "ssl_ca_file='$cafile.crt'\n";
- print $sslconf "ssl_cert_file='$certfile.crt'\n";
- print $sslconf "ssl_key_file='$certfile.key'\n";
- print $sslconf "ssl_crl_file='$crlfile'\n" if defined $crlfile;
- print $sslconf "ssl_crl_dir='$crldir'\n" if defined $crldir;
- close $sslconf;
-
- $node->restart;
- return;
-}
-
-sub configure_hba_for_ssl
-{
- my ($node, $servercidr, $authmethod) = @_;
- my $pgdata = $node->data_dir;
-
- # Only accept SSL connections from $servercidr. Our tests don't depend on this
- # but seems best to keep it as narrow as possible for security reasons.
- #
- # When connecting to certdb, also check the client certificate.
- open my $hba, '>', "$pgdata/pg_hba.conf";
- print $hba
- "# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n";
- print $hba
- "hostssl trustdb md5testuser $servercidr md5\n";
- print $hba
- "hostssl trustdb all $servercidr $authmethod\n";
- print $hba
- "hostssl verifydb ssltestuser $servercidr $authmethod clientcert=verify-full\n";
- print $hba
- "hostssl verifydb anotheruser $servercidr $authmethod clientcert=verify-full\n";
- print $hba
- "hostssl verifydb yetanotheruser $servercidr $authmethod clientcert=verify-ca\n";
- print $hba
- "hostssl certdb all $servercidr cert\n";
- print $hba
- "hostssl certdb_dn all $servercidr cert clientname=DN map=dn\n",
- "hostssl certdb_dn_re all $servercidr cert clientname=DN map=dnre\n",
- "hostssl certdb_cn all $servercidr cert clientname=CN map=cn\n";
- close $hba;
-
- # Also set the ident maps. Note: fields with commas must be quoted
- open my $map, ">", "$pgdata/pg_ident.conf";
- print $map
- "# MAPNAME SYSTEM-USERNAME PG-USERNAME\n",
- "dn \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\" ssltestuser\n",
- "dnre \"/^.*OU=Testing,.*\$\" ssltestuser\n",
- "cn ssltestuser-dn ssltestuser\n";
-
- return;
-}
-
-1;
--
2.34.1
^ permalink raw reply [nested|flat] 74+ messages in thread
* Re: Refactoring SSL tests
@ 2022-02-08 14:24 Daniel Gustafsson <[email protected]>
parent: Andrew Dunstan <[email protected]>
0 siblings, 1 reply; 74+ messages in thread
From: Daniel Gustafsson @ 2022-02-08 14:24 UTC (permalink / raw)
To: Andrew Dunstan <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
> On 7 Feb 2022, at 17:29, Andrew Dunstan <[email protected]> wrote:
> On 2/2/22 14:50, Daniel Gustafsson wrote:
>>> On 2 Feb 2022, at 17:09, Andrew Dunstan <[email protected]> wrote:
>>> On 2/2/22 08:26, Daniel Gustafsson wrote:
>>>> Thoughts? I'm fairly sure there are many crimes against Perl in this patch,
>>>> I'm happy to take pointers on how to improve that.
>>> It feels a bit odd to me from a perl POV. I think it needs to more along
>>> the lines of standard OO patterns. I'll take a stab at that based on
>>> this, might be a few days.
>> That would be great, thanks!
>
> Here's the result of that surgery. It's a little incomplete in that it
> needs some POD adjustment, but I think the code is right - it passes
> testing for me.
Confirmed, it passes all tests for me as well.
> One of the advantages of this, apart from being more idiomatic, is that
> by avoiding the use of package level variables you can have two
> SSL::Server objects, one for OpenSSL and (eventually) one for NSS. This
> was the original motivation for the recent install_path additions to
> PostgreSQL::Test::Cluster, so it complements that work nicely.
Agreed, this version is a clear improvement over my attempt. Thanks!
The attached v2 takes a stab at fixing up the POD sections.
--
Daniel Gustafsson https://vmware.com/
Attachments:
[application/octet-stream] v2-0001-ssl-test-refactoring.patch (44.4K, ../../[email protected]/2-v2-0001-ssl-test-refactoring.patch)
download | inline diff:
From f44fa52fb64cc6c1e8fa652b697386198033b0b2 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 3 Feb 2022 22:16:15 +0100
Subject: [PATCH v2] ssl test refactoring
---
src/test/ssl/t/001_ssltests.pl | 144 +++++------
src/test/ssl/t/002_scram.pl | 21 +-
src/test/ssl/t/003_sslinfo.pl | 33 ++-
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 228 +++++++++++++++++
src/test/ssl/t/SSL/Server.pm | 342 ++++++++++++++++++++++++++
src/test/ssl/t/SSLServer.pm | 219 -----------------
6 files changed, 658 insertions(+), 329 deletions(-)
create mode 100644 src/test/ssl/t/SSL/Backend/OpenSSL.pm
create mode 100644 src/test/ssl/t/SSL/Server.pm
delete mode 100644 src/test/ssl/t/SSLServer.pm
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1fb15ce80..d5383a58ce 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -8,20 +8,24 @@ use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
-use File::Copy;
-
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
-else
+
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
{
- plan tests => 110;
+ $ssl_server->switch_server_cert(@_);
}
#### Some configuration
@@ -36,39 +40,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-# The client's private key must not be world-readable, so take a copy
-# of the key stored in the code tree and update its permissions.
-#
-# This changes to using keys stored in a temporary path for the rest of
-# the tests. To get the full path for inclusion in connection strings, the
-# %key hash can be interrogated.
-my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
-my %key;
-my @keys = (
- "client.key", "client-revoked.key",
- "client-der.key", "client-encrypted-pem.key",
- "client-encrypted-der.key", "client-dn.key");
-foreach my $keyfile (@keys)
-{
- copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
- or die
- "couldn't copy ssl/$keyfile to $cert_tempdir/$keyfile for permissions change: $!";
- chmod 0600, "$cert_tempdir/$keyfile"
- or die "failed to change permissions on $cert_tempdir/$keyfile: $!";
- $key{$keyfile} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
- $key{$keyfile} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
-}
-
-# Also make a copy of that explicitly world-readable. We can't
-# necessarily rely on the file in the source tree having those
-# permissions.
-copy("ssl/client.key", "$cert_tempdir/client_wrongperms.key")
- or die
- "couldn't copy ssl/client_key to $cert_tempdir/client_wrongperms.key for permission change: $!";
-chmod 0644, "$cert_tempdir/client_wrongperms.key"
- or die "failed to change permissions on $cert_tempdir/client_wrongperms.key: $!";
-$key{'client_wrongperms.key'} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
-$key{'client_wrongperms.key'} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
#### Set up the server.
note "setting up data directory";
@@ -83,31 +54,31 @@ $node->start;
# Run this before we lock down access below.
my $result = $node->safe_psql('postgres', "SHOW ssl_library");
-is($result, 'OpenSSL', 'ssl_library parameter');
+is($result, $ssl_server->ssl_library(), 'ssl_library parameter');
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
- 'trust');
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
note "testing password-protected keys";
-open my $sslconf, '>', $node->data_dir . "/sslconfig.conf";
-print $sslconf "ssl=on\n";
-print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
-print $sslconf "ssl_key_file='server-password.key'\n";
-print $sslconf "ssl_passphrase_command='echo wrongpassword'\n";
-close $sslconf;
+switch_server_cert($node,
+ certfile => 'server-cn-only',
+ cafile => 'root+client_ca',
+ keyfile => 'server-password',
+ passphrase_cmd => 'echo wrongpassword',
+ restart => 'no' );
command_fails(
[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
'restart fails with password-protected key file with wrong password');
$node->_update_pid(0);
-open $sslconf, '>', $node->data_dir . "/sslconfig.conf";
-print $sslconf "ssl=on\n";
-print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
-print $sslconf "ssl_key_file='server-password.key'\n";
-print $sslconf "ssl_passphrase_command='echo secret1'\n";
-close $sslconf;
+switch_server_cert($node,
+ certfile => 'server-cn-only',
+ cafile => 'root+client_ca',
+ keyfile => 'server-password',
+ passphrase_cmd => 'echo secret1',
+ restart => 'no');
command_ok(
[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
@@ -140,7 +111,7 @@ command_ok(
note "running client tests";
-switch_server_cert($node, 'server-cn-only');
+switch_server_cert($node, certfile => 'server-cn-only');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -254,7 +225,7 @@ $node->connect_fails(
);
# Test Subject Alternative Names.
-switch_server_cert($node, 'server-multiple-alt-names');
+switch_server_cert($node, certfile => 'server-multiple-alt-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -283,7 +254,7 @@ $node->connect_fails(
# Test certificate with a single Subject Alternative Name. (this gives a
# slightly different error message, that's all)
-switch_server_cert($node, 'server-single-alt-name');
+switch_server_cert($node, certfile => 'server-single-alt-name');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -307,7 +278,7 @@ $node->connect_fails(
# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
# should be ignored when the certificate has both.
-switch_server_cert($node, 'server-cn-and-alt-names');
+switch_server_cert($node, certfile => 'server-cn-and-alt-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -325,7 +296,7 @@ $node->connect_fails(
# Finally, test a server certificate that has no CN or SANs. Of course, that's
# not a very sensible certificate, but libpq should handle it gracefully.
-switch_server_cert($node, 'server-no-names');
+switch_server_cert($node, certfile => 'server-no-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
@@ -340,7 +311,7 @@ $node->connect_fails(
qr/could not get server's host name from server certificate/);
# Test that the CRL works
-switch_server_cert($node, 'server-revoked');
+switch_server_cert($node, certfile => 'server-revoked');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -406,34 +377,34 @@ $node->connect_fails(
# correct client cert in unencrypted PEM
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
"certificate authorization succeeds with correct client cert in PEM format"
);
# correct client cert in unencrypted DER
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-der.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-der.key'),
"certificate authorization succeeds with correct client cert in DER format"
);
# correct client cert in encrypted PEM
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword='dUmmyP^#+'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword='dUmmyP^#+'",
"certificate authorization succeeds with correct client cert in encrypted PEM format"
);
# correct client cert in encrypted DER
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-der.key'} sslpassword='dUmmyP^#+'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-der.key') . " sslpassword='dUmmyP^#+'",
"certificate authorization succeeds with correct client cert in encrypted DER format"
);
# correct client cert in encrypted PEM with wrong password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword='wrong'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword='wrong'",
"certificate authorization fails with correct client cert and wrong password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": bad decrypt\E!
+ qr!private key file \".*client-encrypted-pem\.key\": bad decrypt!,
);
@@ -441,7 +412,7 @@ $node->connect_fails(
my $dn_connstr = "$common_connstr dbname=certdb_dn";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with DN mapping",
log_like => [
qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/
@@ -451,14 +422,14 @@ $node->connect_ok(
$dn_connstr = "$common_connstr dbname=certdb_dn_re";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with DN regex mapping");
# same thing but using explicit CN
$dn_connstr = "$common_connstr dbname=certdb_cn";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with CN mapping",
# the full DN should still be used as the authenticated identity
log_like => [
@@ -476,18 +447,18 @@ TODO:
# correct client cert in encrypted PEM with empty password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword=''",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword=''",
"certificate authorization fails with correct client cert and empty password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": processing error\E!
+ qr!private key file \".*client-encrypted-pem\.key\": processing error!
);
# correct client cert in encrypted PEM with no password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key'),
"certificate authorization fails with correct client cert and no password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": processing error\E!
+ qr!private key file \".*client-encrypted-pem\.key\": processing error!
);
}
@@ -530,12 +501,12 @@ command_like(
'-P',
'null=_null_',
'-d',
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
'-c',
"SELECT * FROM pg_stat_ssl WHERE pid = pg_backend_pid()"
],
qr{^pid,ssl,version,cipher,bits,client_dn,client_serial,issuer_dn\r?\n
- ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,/CN=ssltestuser,$serialno,\Q/CN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx,
+ ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,/?CN=ssltestuser,$serialno,/?\QCN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx,
'pg_stat_ssl with client certificate');
# client key with wrong permissions
@@ -544,16 +515,16 @@ SKIP:
skip "Permissions check not enforced on Windows", 2 if ($windows_os);
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client_wrongperms.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client_wrongperms.key'),
"certificate authorization fails because of file permissions",
expected_stderr =>
- qr!\Qprivate key file "$key{'client_wrongperms.key'}" has group or world access\E!
+ qr!private key file \".*client_wrongperms\.key\" has group or world access!
);
}
# client cert belonging to another user
$node->connect_fails(
- "$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=anotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"certificate authorization fails with client cert belonging to another user",
expected_stderr =>
qr/certificate authentication failed for user "anotheruser"/,
@@ -563,7 +534,7 @@ $node->connect_fails(
# revoked client cert
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=$key{'client-revoked.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'),
"certificate authorization fails with revoked client cert",
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
# revoked certificates should not authenticate the user
@@ -576,13 +547,13 @@ $common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-full succeeds with matching username and Common Name",
# verify-full does not provide authentication
log_unlike => [qr/connection authenticated:/],);
$node->connect_fails(
- "$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=anotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-full fails with mismatching username and Common Name",
expected_stderr =>
qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
@@ -592,15 +563,15 @@ $node->connect_fails(
# Check that connecting with auth-optionverify-ca in pg_hba :
# works, when username doesn't match Common Name
$node->connect_ok(
- "$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=yetanotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
# verify-full does not provide authentication
log_unlike => [qr/connection authenticated:/],);
# intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
-switch_server_cert($node, 'server-cn-only', 'root_ca');
+switch_server_cert($node, certfile => 'server-cn-only', cafile => 'root_ca');
$common_connstr =
- "user=ssltestuser dbname=certdb sslkey=$key{'client.key'} sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
+ "user=ssltestuser dbname=certdb " . sslkey('client.key') . " sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
$node->connect_ok(
"$common_connstr sslmode=require sslcert=ssl/client+client_ca.crt",
@@ -611,11 +582,12 @@ $node->connect_fails(
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/);
# test server-side CRL directory
-switch_server_cert($node, 'server-cn-only', undef, undef,
- 'root+client-crldir');
+switch_server_cert($node, certfile => 'server-cn-only', crldir => 'root+client-crldir');
# revoked client cert
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=$key{'client-revoked.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'),
"certificate authorization fails with revoked client cert with server-side CRL directory",
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
+
+done_testing();
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 86312be88c..bd7a84cec2 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -14,13 +14,24 @@ use File::Copy;
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
+{
+ $ssl_server->switch_server_cert(@_);
+}
+
+
# This is the hostname used to connect to the server.
my $SERVERHOSTADDR = '127.0.0.1';
# This is the pattern to use in pg_hba.conf to match incoming connections.
@@ -30,8 +41,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
my $supports_tls_server_end_point =
check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
-my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
-
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
@@ -48,9 +57,9 @@ $ENV{PGPORT} = $node->port;
$node->start;
# Configure server for SSL connections, with password handling.
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
"scram-sha-256", 'password' => "pass", 'password_enc' => "scram-sha-256");
-switch_server_cert($node, 'server-cn-only');
+switch_server_cert($node, certfile => 'server-cn-only');
$ENV{PGPASSWORD} = "pass";
$common_connstr =
"dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
@@ -118,4 +127,4 @@ $node->connect_ok(
qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
]);
-done_testing($number_of_tests);
+done_testing();
diff --git a/src/test/ssl/t/003_sslinfo.pl b/src/test/ssl/t/003_sslinfo.pl
index 8c760b39db..bc555048da 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -12,18 +12,24 @@ use File::Copy;
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
-else
+
+#### Some configuration
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
{
- plan tests => 13;
+ $ssl_server->switch_server_cert(@_);
}
-#### Some configuration
# This is the hostname used to connect to the server. This cannot be a
# hostname, because the server certificate is always for the domain
@@ -35,17 +41,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-# The client's private key must not be world-readable, so take a copy
-# of the key stored in the code tree and update its permissions.
-my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
-my $client_tmp_key = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_ext.key");
-copy("ssl/client_ext.key", "$cert_tempdir/client_ext.key")
- or die
- "couldn't copy ssl/client_ext.key to $cert_tempdir/client_ext.key for permissions change: $!";
-chmod 0600, "$cert_tempdir/client_ext.key"
- or die "failed to change permissions on $cert_tempdir/client_ext.key: $!";
-$client_tmp_key =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
-
#### Set up the server.
note "setting up data directory";
@@ -58,17 +53,17 @@ $ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
'trust', extensions => [ qw(sslinfo) ]);
# We aren't using any CRL's in this suite so we can keep using server-revoked
# as server certificate for simple client.crt connection much like how the
# 001 test does.
-switch_server_cert($node, 'server-revoked');
+switch_server_cert($node, certfile => 'server-revoked');
$common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR " .
- "user=ssltestuser sslcert=ssl/client_ext.crt sslkey=$client_tmp_key";
+ "user=ssltestuser sslcert=ssl/client_ext.crt " . sslkey('client_ext.key');
# Make sure we can connect even though previous test suites have established this
$node->connect_ok(
@@ -135,3 +130,5 @@ $result = $node->safe_psql("certdb",
"SELECT value, critical FROM ssl_extension_info() WHERE name = 'basicConstraints';",
connstr => $common_connstr);
is($result, 'CA:FALSE|t', 'extract extension from cert');
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
new file mode 100644
index 0000000000..f8dc4b7ad9
--- /dev/null
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -0,0 +1,228 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Backend::OpenSSL
+
+=head1 SYNOPSIS
+
+ use SSL::Backend::OpenSSL;
+
+ my $backend = SSL::backend::OpenSSL->new();
+
+ $backend->init($pgdata);
+
+=head1 DESCRIPTION
+
+SSL::Backend::OpenSSL implements the library specific parts in SSL::Server
+for a PostgreSQL cluster compiled against OpenSSL.
+
+=cut
+
+package SSL::Backend::OpenSSL;
+
+use strict;
+use warnings;
+use File::Basename;
+use File::Copy;
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Backend::OpenSSL->new()
+
+Create a new instance of the OpenSSL backend.
+
+=cut
+
+sub new
+{
+ my ($class) = @_;
+
+ my $self = { _library => 'OpenSSL', key => {} };
+
+ bless $self, $class;
+
+ return $self;
+}
+
+=pod
+
+=item $backend->init(pgdata)
+
+Install certificates, keys and CRL files required to run the tests against an
+OpenSSL backend.
+
+=cut
+
+sub init
+{
+ my ($self, $pgdata) = @_;
+
+ # Install server certificates and keys into the cluster data directory.
+ _copy_files("ssl/server-*.crt", $pgdata);
+ _copy_files("ssl/server-*.key", $pgdata);
+ chmod(0600, glob "$pgdata/server-*.key")
+ or die "failed to change permissions on server keys: $!";
+ _copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root_ca.crt", $pgdata);
+ _copy_files("ssl/root+client.crl", $pgdata);
+ mkdir("$pgdata/root+client-crldir")
+ or die "unable to create server CRL dir $pgdata/root+client-crldir: $!";
+ _copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
+
+ # The client's private key must not be world-readable, so take a copy
+ # of the key stored in the code tree and update its permissions.
+ #
+ # This changes to using keys stored in a temporary path for the rest of
+ # the tests. To get the full path for inclusion in connection strings, the
+ # %key hash can be interrogated.
+ my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
+ my @keys = (
+ "client.key", "client-revoked.key",
+ "client-der.key", "client-encrypted-pem.key",
+ "client-encrypted-der.key", "client-dn.key",
+ "client_ext.key");
+ foreach my $keyfile (@keys)
+ {
+ copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
+ or die
+ "couldn't copy ssl/$keyfile to $cert_tempdir/$keyfile for permissions change: $!";
+ chmod 0600, "$cert_tempdir/$keyfile"
+ or die "failed to change permissions on $cert_tempdir/$keyfile: $!";
+ $self->{key}->{$keyfile} =
+ PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
+ $self->{key}->{$keyfile} =~ s!\\!/!g
+ if $PostgreSQL::Test::Utils::windows_os;
+ }
+
+ # Also make a copy of client.key explicitly world-readable in order to be
+ # able to test incorrect permissions. We can't necessarily rely on the
+ # file in the source tree having those permissions.
+ copy("ssl/client.key", "$cert_tempdir/client_wrongperms.key")
+ or die
+ "couldn't copy ssl/client_key to $cert_tempdir/client_wrongperms.key for permission change: $!";
+ chmod 0644, "$cert_tempdir/client_wrongperms.key"
+ or die "failed to change permissions on $cert_tempdir/client_wrongperms.key: $!";
+ $self->{key}->{'client_wrongperms.key'} =
+ PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
+ $self->key->{'client_wrongperms.key'} =~ s!\\!/!g
+ if $PostgreSQL::Test::Utils::windows_os;
+}
+
+=pod
+
+=item $backend->get_sslkey(key)
+
+Get an 'sslkey' connection string parameter for the specified B<key> which has
+the correct path for direct inclusion in a connection string.
+
+=cut
+
+sub get_sslkey
+{
+ my ($self, $keyfile) = @_;
+
+ return " sslkey=$self->{key}->{$keyfile}";
+}
+
+=pod
+
+=item $backend->set_server_cert(params)
+
+Change the configuration to use given server cert, key and crl file(s). The
+following paramters are supported:
+
+=over
+
+=item cafile => B<value>
+
+The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
+default to 'root+client_ca.crt'.
+
+=item certfile => B<value>
+
+The server certificate file to use for the C<ssl_cert_file> GUC.
+
+=item keyfile => B<value>
+
+The private key file to use for the C<ssl_key_file GUC>. If omitted it will
+default to the B<certfile>.key.
+
+=item crlfile => B<value>
+
+The CRL file to use for the C<ssl_crl_file> GUC. If omitted it will default to
+'root+client.crl'.
+
+=item crldir => B<value>
+
+The CRL directory to use for the C<ssl_crl_dir> GUC. If omitted,
+C<no ssl_crl_dir> configuration parameter will be set.
+
+=back
+
+=cut
+
+sub set_server_cert
+{
+ my ($self, $params) = @_;
+
+ $params->{cafile} = 'root+client_ca' unless defined $params->{cafile};
+ $params->{crlfile} = 'root+client.crl' unless defined $params->{crlfile};
+ $params->{keyfile} = $params->{certfile} unless defined $params->{keyfile};
+
+ my $sslconf =
+ "ssl_ca_file='$params->{cafile}.crt'\n"
+ . "ssl_cert_file='$params->{certfile}.crt'\n"
+ . "ssl_key_file='$params->{keyfile}.key'\n"
+ . "ssl_crl_file='$params->{crlfile}'\n";
+ $sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
+ if defined $params->{crldir};
+
+ return $sslconf;
+}
+
+=pod
+
+=item $backend->get_library()
+
+Returns the name of the SSL library, in this case "OpenSSL".
+
+=cut
+
+sub get_library
+{
+ my ($self) = @_;
+
+ return $self->{_library};
+}
+
+# Internal method for copying a set of files, taking into account wildcards
+sub _copy_files
+{
+ my $orig = shift;
+ my $dest = shift;
+
+ my @orig_files = glob $orig;
+ foreach my $orig_file (@orig_files)
+ {
+ my $base_file = basename($orig_file);
+ copy($orig_file, "$dest/$base_file")
+ or die "Could not copy $orig_file to $dest";
+ }
+ return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
new file mode 100644
index 0000000000..479a872d6a
--- /dev/null
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -0,0 +1,342 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Server - Class for setting up SSL in a PostgreSQL Cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use SSL::Server;
+
+ # Create a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+
+ # Initialize and start the new cluster
+ $node->init;
+ $node->start;
+
+ # Initialize SSL Server functionality for the cluster
+ my $ssl_server = SSL::Server->new();
+
+ # Configure SSL on the newly formed cluster
+ $server->configure_test_server_for_ssl($node, '127.0.0.1', '127.0.0.1/32', 'trust');
+
+=head1 DESCRIPTION
+
+SSL::Server configures an existing test cluster, for the SSL regression tests.
+
+The server is configured as follows:
+
+=over
+
+=item * SSL enabled, with the server certificate specified by arguments to switch_server_cert function.
+
+=item * reject non-SSL connections
+
+=item * a database called trustdb that lets anyone in
+
+=item * another database called certdb that uses certificate authentication, ie. the client must present a valid certificate signed by the client CA
+
+=back
+
+The server is configured to only accept connections from localhost. If you
+want to run the client from another host, you'll have to configure that
+manually.
+
+Note: Someone running these test could have key or certificate files in their
+~/.postgresql/, which would interfere with the tests. The way to override that
+is to specify sslcert=invalid and/or sslrootcert=invalid if no actual
+certificate is used for a particular test. libpq will ignore specifications
+that name nonexisting files. (sslkey and sslcrl do not need to specified
+explicitly because an invalid sslcert or sslrootcert, respectively, causes
+those to be ignored.)
+
+The SSL::Server module presents a SSL library abstraction to the test writer,
+which in turn use modules in SSL::Backend which implements the SSL library
+specific infrastructure. Currently only OpenSSL is supported.
+
+=cut
+
+package SSL::Server;
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use SSL::Backend::OpenSSL;
+
+sub new
+{
+ my $class = shift;
+ my $flavor = shift || $ENV{with_ssl};
+ die "SSL flavor not defined" unless $flavor;
+ my $self = {};
+ bless $self, $class;
+ if ($flavor =~ /\Aopenssl\z/i)
+ {
+ $self->{flavor} = 'openssl';
+ $self->{backend} = SSL::Backend::OpenSSL->new();
+ }
+ else
+ {
+ die "SSL flavor $flavor unknown";
+ }
+ return $self;
+}
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item sslkey(filename)
+
+Return a C<sslkey> construct for the specified key for use in a connection
+string.
+
+=cut
+
+sub sslkey
+{
+ my $self = shift;
+ my $keyfile = shift;
+ my $backend = $self->{backend};
+
+ return $backend->get_sslkey($keyfile);
+}
+
+=pod
+
+=item $server->configure_test_server_for_ssl(node, host, cidr, auth, params)
+
+Configure the cluster specified by B<node> or listening on SSL connections.
+The following databases will be created in the cluster: trustdb, certdb,
+certdb_dn, certdb_dn_re, certdb_cn, verifydb. The following users will be
+created in the cluster: ssltestuser, md5testuser, anotheruser, yetanotheruser.
+If B<< $params{password} >> is set, it will be used as password for all users
+with the password encoding B<< $params{password_enc} >> (except for md5testuser
+which always have MD5). Extensions defined in B<< @{$params{extension}} >>
+will be created in all the above created databases. B<host> is used for
+C<listen_addresses> and B<cidr> for configuring C<pg_hba.conf>.
+
+=cut
+
+sub configure_test_server_for_ssl
+{
+ my $self=shift;
+ my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
+ my $backend = $self->{backend};
+ my $pgdata = $node->data_dir;
+
+ my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
+
+ # Create test users and databases
+ $node->psql('postgres', "CREATE USER ssltestuser");
+ $node->psql('postgres', "CREATE USER md5testuser");
+ $node->psql('postgres', "CREATE USER anotheruser");
+ $node->psql('postgres', "CREATE USER yetanotheruser");
+
+ foreach my $db (@databases)
+ {
+ $node->psql('postgres', "CREATE DATABASE $db");
+ }
+
+ # Update password of each user as needed.
+ if (defined($params{password}))
+ {
+ die "Password encryption must be specified when password is set"
+ unless defined($params{password_enc});
+
+ $node->psql('postgres',
+ "SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
+ );
+ # A special user that always has an md5-encrypted password
+ $node->psql('postgres',
+ "SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
+ );
+ $node->psql('postgres',
+ "SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
+ );
+ }
+
+ # Create any extensions requested in the setup
+ if (defined($params{extensions}))
+ {
+ foreach my $extension (@{$params{extensions}})
+ {
+ foreach my $db (@databases)
+ {
+ $node->psql($db, "CREATE EXTENSION $extension CASCADE;");
+ }
+ }
+ }
+
+ # enable logging etc.
+ open my $conf, '>>', "$pgdata/postgresql.conf";
+ print $conf "fsync=off\n";
+ print $conf "log_connections=on\n";
+ print $conf "log_hostname=on\n";
+ print $conf "listen_addresses='$serverhost'\n";
+ print $conf "log_statement=all\n";
+
+ # enable SSL and set up server key
+ print $conf "include 'sslconfig.conf'\n";
+
+ close $conf;
+
+ # SSL configuration will be placed here
+ open my $sslconf, '>', "$pgdata/sslconfig.conf";
+ close $sslconf;
+
+ # Perform backend specific configuration
+ $backend->init($pgdata);
+
+ # Stop and restart server to load new listen_addresses.
+ $node->restart;
+
+ # Change pg_hba after restart because hostssl requires ssl=on
+ _configure_hba_for_ssl($node, $servercidr, $authmethod);
+
+ return;
+}
+
+=pod
+
+=item $server->ssl_library()
+
+Get the name of the currently used SSL backend.
+
+=cut
+
+sub ssl_library
+{
+ my $self = shift;
+ my $backend = $self->{backend};
+
+ return $backend->get_library();
+}
+
+=pod
+
+=item switch_server_cert(params)
+
+Change the configuration to use the given set of certificate, key, ca and
+CRL, and potentially reload the configuration by restarting the server so
+that the configuration takes effect. Restarting is the default, passing
+B<< $params{restart} >> => 'no' opts out of it leaving the server running.
+The following params are supported:
+
+=over
+
+=item cafile => B<value>
+
+The CA certificate to use. Implementation is SSL backend specific.
+
+=item certfile => B<value>
+
+The certificate file to use. Implementation is SSL backend specific.
+
+=item keyfile => B<value>
+
+The private key to to use. Implementation is SSL backend specific.
+
+=item crlfile => B<value>
+
+The CRL file to use. Implementation is SSL backend specific.
+
+=item crldir => B<value>
+
+The CRL directory to use. Implementation is SSL backend specific.
+
+=item passphrase_cmd => B<value>
+
+The passphrase command to use. If not set, an empty passphrase command will
+be set.
+
+=item restart => B<value>
+
+If set to 'no', the server won't be restarted after updating the settings.
+If omitted, or any other value is passed, the server will be restarted before
+returning.
+
+=back
+
+=cut
+
+sub switch_server_cert
+{
+ my $self = shift;
+ my $node = shift;
+ my $backend = $self->{backend};
+ my %params = @_;
+ my $pgdata = $node->data_dir;
+
+ open my $sslconf, '>', "$pgdata/sslconfig.conf";
+ print $sslconf "ssl=on\n";
+ print $sslconf $backend->set_server_cert(\%params);
+ print $sslconf "ssl_passphrase_command='" . $params{passphrase_cmd} . "'\n"
+ if defined $params{passphrase_cmd};
+ close $sslconf;
+
+ return if (defined($params{restart}) && $params{restart} eq 'no');
+
+ $node->restart;
+ return;
+}
+
+
+# Internal function for configuring pg_hba.conf for SSL connections.
+sub _configure_hba_for_ssl
+{
+ my ($node, $servercidr, $authmethod) = @_;
+ my $pgdata = $node->data_dir;
+
+ # Only accept SSL connections from $servercidr. Our tests don't depend on this
+ # but seems best to keep it as narrow as possible for security reasons.
+ #
+ # When connecting to certdb, also check the client certificate.
+ open my $hba, '>', "$pgdata/pg_hba.conf";
+ print $hba
+ "# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n";
+ print $hba
+ "hostssl trustdb md5testuser $servercidr md5\n";
+ print $hba
+ "hostssl trustdb all $servercidr $authmethod\n";
+ print $hba
+ "hostssl verifydb ssltestuser $servercidr $authmethod clientcert=verify-full\n";
+ print $hba
+ "hostssl verifydb anotheruser $servercidr $authmethod clientcert=verify-full\n";
+ print $hba
+ "hostssl verifydb yetanotheruser $servercidr $authmethod clientcert=verify-ca\n";
+ print $hba
+ "hostssl certdb all $servercidr cert\n";
+ print $hba
+ "hostssl certdb_dn all $servercidr cert clientname=DN map=dn\n",
+ "hostssl certdb_dn_re all $servercidr cert clientname=DN map=dnre\n",
+ "hostssl certdb_cn all $servercidr cert clientname=CN map=cn\n";
+ close $hba;
+
+ # Also set the ident maps. Note: fields with commas must be quoted
+ open my $map, ">", "$pgdata/pg_ident.conf";
+ print $map
+ "# MAPNAME SYSTEM-USERNAME PG-USERNAME\n",
+ "dn \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\" ssltestuser\n",
+ "dnre \"/^.*OU=Testing,.*\$\" ssltestuser\n",
+ "cn ssltestuser-dn ssltestuser\n";
+
+ return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm
deleted file mode 100644
index c85c6fd997..0000000000
--- a/src/test/ssl/t/SSLServer.pm
+++ /dev/null
@@ -1,219 +0,0 @@
-
-# Copyright (c) 2021-2022, PostgreSQL Global Development Group
-
-# This module sets up a test server, for the SSL regression tests.
-#
-# The server is configured as follows:
-#
-# - SSL enabled, with the server certificate specified by argument to
-# switch_server_cert function.
-# - ssl/root+client_ca.crt as the CA root for validating client certs.
-# - reject non-SSL connections
-# - a database called trustdb that lets anyone in
-# - another database called certdb that uses certificate authentication, ie.
-# the client must present a valid certificate signed by the client CA
-#
-# The server is configured to only accept connections from localhost. If you
-# want to run the client from another host, you'll have to configure that
-# manually.
-#
-# Note: Someone running these test could have key or certificate files
-# in their ~/.postgresql/, which would interfere with the tests. The
-# way to override that is to specify sslcert=invalid and/or
-# sslrootcert=invalid if no actual certificate is used for a
-# particular test. libpq will ignore specifications that name
-# nonexisting files. (sslkey and sslcrl do not need to specified
-# explicitly because an invalid sslcert or sslrootcert, respectively,
-# causes those to be ignored.)
-
-package SSLServer;
-
-use strict;
-use warnings;
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use File::Basename;
-use File::Copy;
-use Test::More;
-
-use Exporter 'import';
-our @EXPORT = qw(
- configure_test_server_for_ssl
- switch_server_cert
-);
-
-# Copy a set of files, taking into account wildcards
-sub copy_files
-{
- my $orig = shift;
- my $dest = shift;
-
- my @orig_files = glob $orig;
- foreach my $orig_file (@orig_files)
- {
- my $base_file = basename($orig_file);
- copy($orig_file, "$dest/$base_file")
- or die "Could not copy $orig_file to $dest";
- }
- return;
-}
-
-# serverhost: what to put in listen_addresses, e.g. '127.0.0.1'
-# servercidr: what to put in pg_hba.conf, e.g. '127.0.0.1/32'
-sub configure_test_server_for_ssl
-{
- my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
- my $pgdata = $node->data_dir;
-
- my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
-
- # Create test users and databases
- $node->psql('postgres', "CREATE USER ssltestuser");
- $node->psql('postgres', "CREATE USER md5testuser");
- $node->psql('postgres', "CREATE USER anotheruser");
- $node->psql('postgres', "CREATE USER yetanotheruser");
-
- foreach my $db (@databases)
- {
- $node->psql('postgres', "CREATE DATABASE $db");
- }
-
- # Update password of each user as needed.
- if (defined($params{password}))
- {
- die "Password encryption must be specified when password is set"
- unless defined($params{password_enc});
-
- $node->psql('postgres',
- "SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
- );
- # A special user that always has an md5-encrypted password
- $node->psql('postgres',
- "SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
- );
- $node->psql('postgres',
- "SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
- );
- }
-
- # Create any extensions requested in the setup
- if (defined($params{extensions}))
- {
- foreach my $extension (@{$params{extensions}})
- {
- foreach my $db (@databases)
- {
- $node->psql($db, "CREATE EXTENSION $extension CASCADE;");
- }
- }
- }
-
- # enable logging etc.
- open my $conf, '>>', "$pgdata/postgresql.conf";
- print $conf "fsync=off\n";
- print $conf "log_connections=on\n";
- print $conf "log_hostname=on\n";
- print $conf "listen_addresses='$serverhost'\n";
- print $conf "log_statement=all\n";
-
- # enable SSL and set up server key
- print $conf "include 'sslconfig.conf'\n";
-
- close $conf;
-
- # ssl configuration will be placed here
- open my $sslconf, '>', "$pgdata/sslconfig.conf";
- close $sslconf;
-
- # Copy all server certificates and keys, and client root cert, to the data dir
- copy_files("ssl/server-*.crt", $pgdata);
- copy_files("ssl/server-*.key", $pgdata);
- chmod(0600, glob "$pgdata/server-*.key") or die $!;
- copy_files("ssl/root+client_ca.crt", $pgdata);
- copy_files("ssl/root_ca.crt", $pgdata);
- copy_files("ssl/root+client.crl", $pgdata);
- mkdir("$pgdata/root+client-crldir");
- copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
-
- # Stop and restart server to load new listen_addresses.
- $node->restart;
-
- # Change pg_hba after restart because hostssl requires ssl=on
- configure_hba_for_ssl($node, $servercidr, $authmethod);
-
- return;
-}
-
-# Change the configuration to use given server cert file, and reload
-# the server so that the configuration takes effect.
-sub switch_server_cert
-{
- my $node = $_[0];
- my $certfile = $_[1];
- my $cafile = $_[2] || "root+client_ca";
- my $crlfile = "root+client.crl";
- my $crldir;
- my $pgdata = $node->data_dir;
-
- # defaults to use crl file
- if (defined $_[3] || defined $_[4])
- {
- $crlfile = $_[3];
- $crldir = $_[4];
- }
-
- open my $sslconf, '>', "$pgdata/sslconfig.conf";
- print $sslconf "ssl=on\n";
- print $sslconf "ssl_ca_file='$cafile.crt'\n";
- print $sslconf "ssl_cert_file='$certfile.crt'\n";
- print $sslconf "ssl_key_file='$certfile.key'\n";
- print $sslconf "ssl_crl_file='$crlfile'\n" if defined $crlfile;
- print $sslconf "ssl_crl_dir='$crldir'\n" if defined $crldir;
- close $sslconf;
-
- $node->restart;
- return;
-}
-
-sub configure_hba_for_ssl
-{
- my ($node, $servercidr, $authmethod) = @_;
- my $pgdata = $node->data_dir;
-
- # Only accept SSL connections from $servercidr. Our tests don't depend on this
- # but seems best to keep it as narrow as possible for security reasons.
- #
- # When connecting to certdb, also check the client certificate.
- open my $hba, '>', "$pgdata/pg_hba.conf";
- print $hba
- "# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n";
- print $hba
- "hostssl trustdb md5testuser $servercidr md5\n";
- print $hba
- "hostssl trustdb all $servercidr $authmethod\n";
- print $hba
- "hostssl verifydb ssltestuser $servercidr $authmethod clientcert=verify-full\n";
- print $hba
- "hostssl verifydb anotheruser $servercidr $authmethod clientcert=verify-full\n";
- print $hba
- "hostssl verifydb yetanotheruser $servercidr $authmethod clientcert=verify-ca\n";
- print $hba
- "hostssl certdb all $servercidr cert\n";
- print $hba
- "hostssl certdb_dn all $servercidr cert clientname=DN map=dn\n",
- "hostssl certdb_dn_re all $servercidr cert clientname=DN map=dnre\n",
- "hostssl certdb_cn all $servercidr cert clientname=CN map=cn\n";
- close $hba;
-
- # Also set the ident maps. Note: fields with commas must be quoted
- open my $map, ">", "$pgdata/pg_ident.conf";
- print $map
- "# MAPNAME SYSTEM-USERNAME PG-USERNAME\n",
- "dn \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\" ssltestuser\n",
- "dnre \"/^.*OU=Testing,.*\$\" ssltestuser\n",
- "cn ssltestuser-dn ssltestuser\n";
-
- return;
-}
-
-1;
--
2.24.3 (Apple Git-128)
^ permalink raw reply [nested|flat] 74+ messages in thread
* Re: Refactoring SSL tests
@ 2022-02-08 15:46 Andrew Dunstan <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 74+ messages in thread
From: Andrew Dunstan @ 2022-02-08 15:46 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On 2/8/22 09:24, Daniel Gustafsson wrote:
>
> The attached v2 takes a stab at fixing up the POD sections.
There a capitalization typo in SSL/Backend/OpenSSL.pm - looks like
that's my fault:
+Â my $backend = SSL::backend::OpenSSL->new();
Also, I think we should document that SSL::Server::new() takes an
optional flavor parameter, in the absence of which it uses
$ENV{with_ssl}, and that 'openssl' is the only currently supported value.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 74+ messages in thread
* Re: Refactoring SSL tests
@ 2022-02-09 13:11 Daniel Gustafsson <[email protected]>
parent: Andrew Dunstan <[email protected]>
0 siblings, 1 reply; 74+ messages in thread
From: Daniel Gustafsson @ 2022-02-09 13:11 UTC (permalink / raw)
To: Andrew Dunstan <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
> On 8 Feb 2022, at 16:46, Andrew Dunstan <[email protected]> wrote:
> There a capitalization typo in SSL/Backend/OpenSSL.pm - looks like
> that's my fault:
>
> + my $backend = SSL::backend::OpenSSL->new();
Fixed.
> Also, I think we should document that SSL::Server::new() takes an
> optional flavor parameter, in the absence of which it uses
> $ENV{with_ssl}, and that 'openssl' is the only currently supported value.
Good point, done in the attached.
--
Daniel Gustafsson https://vmware.com/
Attachments:
[application/octet-stream] v3-0001-ssl-test-refactoring.patch (44.7K, ../../[email protected]/2-v3-0001-ssl-test-refactoring.patch)
download | inline diff:
From 5527d4801eabe5ea3a95727cabd3ad22bedbaa55 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Thu, 3 Feb 2022 22:16:15 +0100
Subject: [PATCH v3] ssl test refactoring
---
src/test/ssl/t/001_ssltests.pl | 144 +++++------
src/test/ssl/t/002_scram.pl | 21 +-
src/test/ssl/t/003_sslinfo.pl | 33 ++-
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 228 +++++++++++++++++
src/test/ssl/t/SSL/Server.pm | 353 ++++++++++++++++++++++++++
src/test/ssl/t/SSLServer.pm | 219 ----------------
6 files changed, 669 insertions(+), 329 deletions(-)
create mode 100644 src/test/ssl/t/SSL/Backend/OpenSSL.pm
create mode 100644 src/test/ssl/t/SSL/Server.pm
delete mode 100644 src/test/ssl/t/SSLServer.pm
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index b1fb15ce80..d5383a58ce 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -8,20 +8,24 @@ use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
-use File::Copy;
-
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
-else
+
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
{
- plan tests => 110;
+ $ssl_server->switch_server_cert(@_);
}
#### Some configuration
@@ -36,39 +40,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-# The client's private key must not be world-readable, so take a copy
-# of the key stored in the code tree and update its permissions.
-#
-# This changes to using keys stored in a temporary path for the rest of
-# the tests. To get the full path for inclusion in connection strings, the
-# %key hash can be interrogated.
-my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
-my %key;
-my @keys = (
- "client.key", "client-revoked.key",
- "client-der.key", "client-encrypted-pem.key",
- "client-encrypted-der.key", "client-dn.key");
-foreach my $keyfile (@keys)
-{
- copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
- or die
- "couldn't copy ssl/$keyfile to $cert_tempdir/$keyfile for permissions change: $!";
- chmod 0600, "$cert_tempdir/$keyfile"
- or die "failed to change permissions on $cert_tempdir/$keyfile: $!";
- $key{$keyfile} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
- $key{$keyfile} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
-}
-
-# Also make a copy of that explicitly world-readable. We can't
-# necessarily rely on the file in the source tree having those
-# permissions.
-copy("ssl/client.key", "$cert_tempdir/client_wrongperms.key")
- or die
- "couldn't copy ssl/client_key to $cert_tempdir/client_wrongperms.key for permission change: $!";
-chmod 0644, "$cert_tempdir/client_wrongperms.key"
- or die "failed to change permissions on $cert_tempdir/client_wrongperms.key: $!";
-$key{'client_wrongperms.key'} = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
-$key{'client_wrongperms.key'} =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
#### Set up the server.
note "setting up data directory";
@@ -83,31 +54,31 @@ $node->start;
# Run this before we lock down access below.
my $result = $node->safe_psql('postgres', "SHOW ssl_library");
-is($result, 'OpenSSL', 'ssl_library parameter');
+is($result, $ssl_server->ssl_library(), 'ssl_library parameter');
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
- 'trust');
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
note "testing password-protected keys";
-open my $sslconf, '>', $node->data_dir . "/sslconfig.conf";
-print $sslconf "ssl=on\n";
-print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
-print $sslconf "ssl_key_file='server-password.key'\n";
-print $sslconf "ssl_passphrase_command='echo wrongpassword'\n";
-close $sslconf;
+switch_server_cert($node,
+ certfile => 'server-cn-only',
+ cafile => 'root+client_ca',
+ keyfile => 'server-password',
+ passphrase_cmd => 'echo wrongpassword',
+ restart => 'no' );
command_fails(
[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
'restart fails with password-protected key file with wrong password');
$node->_update_pid(0);
-open $sslconf, '>', $node->data_dir . "/sslconfig.conf";
-print $sslconf "ssl=on\n";
-print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
-print $sslconf "ssl_key_file='server-password.key'\n";
-print $sslconf "ssl_passphrase_command='echo secret1'\n";
-close $sslconf;
+switch_server_cert($node,
+ certfile => 'server-cn-only',
+ cafile => 'root+client_ca',
+ keyfile => 'server-password',
+ passphrase_cmd => 'echo secret1',
+ restart => 'no');
command_ok(
[ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ],
@@ -140,7 +111,7 @@ command_ok(
note "running client tests";
-switch_server_cert($node, 'server-cn-only');
+switch_server_cert($node, certfile => 'server-cn-only');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -254,7 +225,7 @@ $node->connect_fails(
);
# Test Subject Alternative Names.
-switch_server_cert($node, 'server-multiple-alt-names');
+switch_server_cert($node, certfile => 'server-multiple-alt-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -283,7 +254,7 @@ $node->connect_fails(
# Test certificate with a single Subject Alternative Name. (this gives a
# slightly different error message, that's all)
-switch_server_cert($node, 'server-single-alt-name');
+switch_server_cert($node, certfile => 'server-single-alt-name');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -307,7 +278,7 @@ $node->connect_fails(
# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
# should be ignored when the certificate has both.
-switch_server_cert($node, 'server-cn-and-alt-names');
+switch_server_cert($node, certfile => 'server-cn-and-alt-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
@@ -325,7 +296,7 @@ $node->connect_fails(
# Finally, test a server certificate that has no CN or SANs. Of course, that's
# not a very sensible certificate, but libpq should handle it gracefully.
-switch_server_cert($node, 'server-no-names');
+switch_server_cert($node, certfile => 'server-no-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
@@ -340,7 +311,7 @@ $node->connect_fails(
qr/could not get server's host name from server certificate/);
# Test that the CRL works
-switch_server_cert($node, 'server-revoked');
+switch_server_cert($node, certfile => 'server-revoked');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -406,34 +377,34 @@ $node->connect_fails(
# correct client cert in unencrypted PEM
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
"certificate authorization succeeds with correct client cert in PEM format"
);
# correct client cert in unencrypted DER
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-der.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-der.key'),
"certificate authorization succeeds with correct client cert in DER format"
);
# correct client cert in encrypted PEM
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword='dUmmyP^#+'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword='dUmmyP^#+'",
"certificate authorization succeeds with correct client cert in encrypted PEM format"
);
# correct client cert in encrypted DER
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-der.key'} sslpassword='dUmmyP^#+'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-der.key') . " sslpassword='dUmmyP^#+'",
"certificate authorization succeeds with correct client cert in encrypted DER format"
);
# correct client cert in encrypted PEM with wrong password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword='wrong'",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword='wrong'",
"certificate authorization fails with correct client cert and wrong password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": bad decrypt\E!
+ qr!private key file \".*client-encrypted-pem\.key\": bad decrypt!,
);
@@ -441,7 +412,7 @@ $node->connect_fails(
my $dn_connstr = "$common_connstr dbname=certdb_dn";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with DN mapping",
log_like => [
qr/connection authenticated: identity="CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG" method=cert/
@@ -451,14 +422,14 @@ $node->connect_ok(
$dn_connstr = "$common_connstr dbname=certdb_dn_re";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with DN regex mapping");
# same thing but using explicit CN
$dn_connstr = "$common_connstr dbname=certdb_cn";
$node->connect_ok(
- "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt sslkey=$key{'client-dn.key'}",
+ "$dn_connstr user=ssltestuser sslcert=ssl/client-dn.crt " . sslkey('client-dn.key'),
"certificate authorization succeeds with CN mapping",
# the full DN should still be used as the authenticated identity
log_like => [
@@ -476,18 +447,18 @@ TODO:
# correct client cert in encrypted PEM with empty password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'} sslpassword=''",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key') . " sslpassword=''",
"certificate authorization fails with correct client cert and empty password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": processing error\E!
+ qr!private key file \".*client-encrypted-pem\.key\": processing error!
);
# correct client cert in encrypted PEM with no password
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client-encrypted-pem.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client-encrypted-pem.key'),
"certificate authorization fails with correct client cert and no password in encrypted PEM format",
expected_stderr =>
- qr!\Qprivate key file "$key{'client-encrypted-pem.key'}": processing error\E!
+ qr!private key file \".*client-encrypted-pem\.key\": processing error!
);
}
@@ -530,12 +501,12 @@ command_like(
'-P',
'null=_null_',
'-d',
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
'-c',
"SELECT * FROM pg_stat_ssl WHERE pid = pg_backend_pid()"
],
qr{^pid,ssl,version,cipher,bits,client_dn,client_serial,issuer_dn\r?\n
- ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,/CN=ssltestuser,$serialno,\Q/CN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx,
+ ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,/?CN=ssltestuser,$serialno,/?\QCN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx,
'pg_stat_ssl with client certificate');
# client key with wrong permissions
@@ -544,16 +515,16 @@ SKIP:
skip "Permissions check not enforced on Windows", 2 if ($windows_os);
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client_wrongperms.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client_wrongperms.key'),
"certificate authorization fails because of file permissions",
expected_stderr =>
- qr!\Qprivate key file "$key{'client_wrongperms.key'}" has group or world access\E!
+ qr!private key file \".*client_wrongperms\.key\" has group or world access!
);
}
# client cert belonging to another user
$node->connect_fails(
- "$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=anotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"certificate authorization fails with client cert belonging to another user",
expected_stderr =>
qr/certificate authentication failed for user "anotheruser"/,
@@ -563,7 +534,7 @@ $node->connect_fails(
# revoked client cert
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=$key{'client-revoked.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'),
"certificate authorization fails with revoked client cert",
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/,
# revoked certificates should not authenticate the user
@@ -576,13 +547,13 @@ $common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR";
$node->connect_ok(
- "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-full succeeds with matching username and Common Name",
# verify-full does not provide authentication
log_unlike => [qr/connection authenticated:/],);
$node->connect_fails(
- "$common_connstr user=anotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=anotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-full fails with mismatching username and Common Name",
expected_stderr =>
qr/FATAL: .* "trust" authentication failed for user "anotheruser"/,
@@ -592,15 +563,15 @@ $node->connect_fails(
# Check that connecting with auth-optionverify-ca in pg_hba :
# works, when username doesn't match Common Name
$node->connect_ok(
- "$common_connstr user=yetanotheruser sslcert=ssl/client.crt sslkey=$key{'client.key'}",
+ "$common_connstr user=yetanotheruser sslcert=ssl/client.crt " . sslkey('client.key'),
"auth_option clientcert=verify-ca succeeds with mismatching username and Common Name",
# verify-full does not provide authentication
log_unlike => [qr/connection authenticated:/],);
# intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file
-switch_server_cert($node, 'server-cn-only', 'root_ca');
+switch_server_cert($node, certfile => 'server-cn-only', cafile => 'root_ca');
$common_connstr =
- "user=ssltestuser dbname=certdb sslkey=$key{'client.key'} sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
+ "user=ssltestuser dbname=certdb " . sslkey('client.key') . " sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
$node->connect_ok(
"$common_connstr sslmode=require sslcert=ssl/client+client_ca.crt",
@@ -611,11 +582,12 @@ $node->connect_fails(
expected_stderr => qr/SSL error: tlsv1 alert unknown ca/);
# test server-side CRL directory
-switch_server_cert($node, 'server-cn-only', undef, undef,
- 'root+client-crldir');
+switch_server_cert($node, certfile => 'server-cn-only', crldir => 'root+client-crldir');
# revoked client cert
$node->connect_fails(
- "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=$key{'client-revoked.key'}",
+ "$common_connstr user=ssltestuser sslcert=ssl/client-revoked.crt " . sslkey('client-revoked.key'),
"certificate authorization fails with revoked client cert with server-side CRL directory",
expected_stderr => qr/SSL error: sslv3 alert certificate revoked/);
+
+done_testing();
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 86312be88c..bd7a84cec2 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -14,13 +14,24 @@ use File::Copy;
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
+{
+ $ssl_server->switch_server_cert(@_);
+}
+
+
# This is the hostname used to connect to the server.
my $SERVERHOSTADDR = '127.0.0.1';
# This is the pattern to use in pg_hba.conf to match incoming connections.
@@ -30,8 +41,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
my $supports_tls_server_end_point =
check_pg_config("#define HAVE_X509_GET_SIGNATURE_NID 1");
-my $number_of_tests = $supports_tls_server_end_point ? 11 : 12;
-
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
@@ -48,9 +57,9 @@ $ENV{PGPORT} = $node->port;
$node->start;
# Configure server for SSL connections, with password handling.
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
"scram-sha-256", 'password' => "pass", 'password_enc' => "scram-sha-256");
-switch_server_cert($node, 'server-cn-only');
+switch_server_cert($node, certfile => 'server-cn-only');
$ENV{PGPASSWORD} = "pass";
$common_connstr =
"dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR";
@@ -118,4 +127,4 @@ $node->connect_ok(
qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/
]);
-done_testing($number_of_tests);
+done_testing();
diff --git a/src/test/ssl/t/003_sslinfo.pl b/src/test/ssl/t/003_sslinfo.pl
index 8c760b39db..bc555048da 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -12,18 +12,24 @@ use File::Copy;
use FindBin;
use lib $FindBin::RealBin;
-use SSLServer;
+use SSL::Server;
if ($ENV{with_ssl} ne 'openssl')
{
plan skip_all => 'OpenSSL not supported by this build';
}
-else
+
+#### Some configuration
+my $ssl_server = SSL::Server->new();
+sub sslkey
+{
+ return $ssl_server->sslkey(@_);
+}
+sub switch_server_cert
{
- plan tests => 13;
+ $ssl_server->switch_server_cert(@_);
}
-#### Some configuration
# This is the hostname used to connect to the server. This cannot be a
# hostname, because the server certificate is always for the domain
@@ -35,17 +41,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-# The client's private key must not be world-readable, so take a copy
-# of the key stored in the code tree and update its permissions.
-my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
-my $client_tmp_key = PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_ext.key");
-copy("ssl/client_ext.key", "$cert_tempdir/client_ext.key")
- or die
- "couldn't copy ssl/client_ext.key to $cert_tempdir/client_ext.key for permissions change: $!";
-chmod 0600, "$cert_tempdir/client_ext.key"
- or die "failed to change permissions on $cert_tempdir/client_ext.key: $!";
-$client_tmp_key =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
-
#### Set up the server.
note "setting up data directory";
@@ -58,17 +53,17 @@ $ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR,
'trust', extensions => [ qw(sslinfo) ]);
# We aren't using any CRL's in this suite so we can keep using server-revoked
# as server certificate for simple client.crt connection much like how the
# 001 test does.
-switch_server_cert($node, 'server-revoked');
+switch_server_cert($node, certfile => 'server-revoked');
$common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR " .
- "user=ssltestuser sslcert=ssl/client_ext.crt sslkey=$client_tmp_key";
+ "user=ssltestuser sslcert=ssl/client_ext.crt " . sslkey('client_ext.key');
# Make sure we can connect even though previous test suites have established this
$node->connect_ok(
@@ -135,3 +130,5 @@ $result = $node->safe_psql("certdb",
"SELECT value, critical FROM ssl_extension_info() WHERE name = 'basicConstraints';",
connstr => $common_connstr);
is($result, 'CA:FALSE|t', 'extract extension from cert');
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
new file mode 100644
index 0000000000..4ca7fdba4a
--- /dev/null
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -0,0 +1,228 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Backend::OpenSSL
+
+=head1 SYNOPSIS
+
+ use SSL::Backend::OpenSSL;
+
+ my $backend = SSL::Backend::OpenSSL->new();
+
+ $backend->init($pgdata);
+
+=head1 DESCRIPTION
+
+SSL::Backend::OpenSSL implements the library specific parts in SSL::Server
+for a PostgreSQL cluster compiled against OpenSSL.
+
+=cut
+
+package SSL::Backend::OpenSSL;
+
+use strict;
+use warnings;
+use File::Basename;
+use File::Copy;
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Backend::OpenSSL->new()
+
+Create a new instance of the OpenSSL backend.
+
+=cut
+
+sub new
+{
+ my ($class) = @_;
+
+ my $self = { _library => 'OpenSSL', key => {} };
+
+ bless $self, $class;
+
+ return $self;
+}
+
+=pod
+
+=item $backend->init(pgdata)
+
+Install certificates, keys and CRL files required to run the tests against an
+OpenSSL backend.
+
+=cut
+
+sub init
+{
+ my ($self, $pgdata) = @_;
+
+ # Install server certificates and keys into the cluster data directory.
+ _copy_files("ssl/server-*.crt", $pgdata);
+ _copy_files("ssl/server-*.key", $pgdata);
+ chmod(0600, glob "$pgdata/server-*.key")
+ or die "failed to change permissions on server keys: $!";
+ _copy_files("ssl/root+client_ca.crt", $pgdata);
+ _copy_files("ssl/root_ca.crt", $pgdata);
+ _copy_files("ssl/root+client.crl", $pgdata);
+ mkdir("$pgdata/root+client-crldir")
+ or die "unable to create server CRL dir $pgdata/root+client-crldir: $!";
+ _copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
+
+ # The client's private key must not be world-readable, so take a copy
+ # of the key stored in the code tree and update its permissions.
+ #
+ # This changes to using keys stored in a temporary path for the rest of
+ # the tests. To get the full path for inclusion in connection strings, the
+ # %key hash can be interrogated.
+ my $cert_tempdir = PostgreSQL::Test::Utils::tempdir();
+ my @keys = (
+ "client.key", "client-revoked.key",
+ "client-der.key", "client-encrypted-pem.key",
+ "client-encrypted-der.key", "client-dn.key",
+ "client_ext.key");
+ foreach my $keyfile (@keys)
+ {
+ copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
+ or die
+ "couldn't copy ssl/$keyfile to $cert_tempdir/$keyfile for permissions change: $!";
+ chmod 0600, "$cert_tempdir/$keyfile"
+ or die "failed to change permissions on $cert_tempdir/$keyfile: $!";
+ $self->{key}->{$keyfile} =
+ PostgreSQL::Test::Utils::perl2host("$cert_tempdir/$keyfile");
+ $self->{key}->{$keyfile} =~ s!\\!/!g
+ if $PostgreSQL::Test::Utils::windows_os;
+ }
+
+ # Also make a copy of client.key explicitly world-readable in order to be
+ # able to test incorrect permissions. We can't necessarily rely on the
+ # file in the source tree having those permissions.
+ copy("ssl/client.key", "$cert_tempdir/client_wrongperms.key")
+ or die
+ "couldn't copy ssl/client_key to $cert_tempdir/client_wrongperms.key for permission change: $!";
+ chmod 0644, "$cert_tempdir/client_wrongperms.key"
+ or die "failed to change permissions on $cert_tempdir/client_wrongperms.key: $!";
+ $self->{key}->{'client_wrongperms.key'} =
+ PostgreSQL::Test::Utils::perl2host("$cert_tempdir/client_wrongperms.key");
+ $self->key->{'client_wrongperms.key'} =~ s!\\!/!g
+ if $PostgreSQL::Test::Utils::windows_os;
+}
+
+=pod
+
+=item $backend->get_sslkey(key)
+
+Get an 'sslkey' connection string parameter for the specified B<key> which has
+the correct path for direct inclusion in a connection string.
+
+=cut
+
+sub get_sslkey
+{
+ my ($self, $keyfile) = @_;
+
+ return " sslkey=$self->{key}->{$keyfile}";
+}
+
+=pod
+
+=item $backend->set_server_cert(params)
+
+Change the configuration to use given server cert, key and crl file(s). The
+following paramters are supported:
+
+=over
+
+=item cafile => B<value>
+
+The CA certificate file to use for the C<ssl_ca_file> GUC. If omitted it will
+default to 'root+client_ca.crt'.
+
+=item certfile => B<value>
+
+The server certificate file to use for the C<ssl_cert_file> GUC.
+
+=item keyfile => B<value>
+
+The private key file to use for the C<ssl_key_file GUC>. If omitted it will
+default to the B<certfile>.key.
+
+=item crlfile => B<value>
+
+The CRL file to use for the C<ssl_crl_file> GUC. If omitted it will default to
+'root+client.crl'.
+
+=item crldir => B<value>
+
+The CRL directory to use for the C<ssl_crl_dir> GUC. If omitted,
+C<no ssl_crl_dir> configuration parameter will be set.
+
+=back
+
+=cut
+
+sub set_server_cert
+{
+ my ($self, $params) = @_;
+
+ $params->{cafile} = 'root+client_ca' unless defined $params->{cafile};
+ $params->{crlfile} = 'root+client.crl' unless defined $params->{crlfile};
+ $params->{keyfile} = $params->{certfile} unless defined $params->{keyfile};
+
+ my $sslconf =
+ "ssl_ca_file='$params->{cafile}.crt'\n"
+ . "ssl_cert_file='$params->{certfile}.crt'\n"
+ . "ssl_key_file='$params->{keyfile}.key'\n"
+ . "ssl_crl_file='$params->{crlfile}'\n";
+ $sslconf .= "ssl_crl_dir='$params->{crldir}'\n"
+ if defined $params->{crldir};
+
+ return $sslconf;
+}
+
+=pod
+
+=item $backend->get_library()
+
+Returns the name of the SSL library, in this case "OpenSSL".
+
+=cut
+
+sub get_library
+{
+ my ($self) = @_;
+
+ return $self->{_library};
+}
+
+# Internal method for copying a set of files, taking into account wildcards
+sub _copy_files
+{
+ my $orig = shift;
+ my $dest = shift;
+
+ my @orig_files = glob $orig;
+ foreach my $orig_file (@orig_files)
+ {
+ my $base_file = basename($orig_file);
+ copy($orig_file, "$dest/$base_file")
+ or die "Could not copy $orig_file to $dest";
+ }
+ return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
new file mode 100644
index 0000000000..af25ac3b29
--- /dev/null
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -0,0 +1,353 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+SSL::Server - Class for setting up SSL in a PostgreSQL Cluster
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Cluster;
+ use SSL::Server;
+
+ # Create a new cluster
+ my $node = PostgreSQL::Test::Cluster->new('primary');
+
+ # Initialize and start the new cluster
+ $node->init;
+ $node->start;
+
+ # Initialize SSL Server functionality for the cluster
+ my $ssl_server = SSL::Server->new();
+
+ # Configure SSL on the newly formed cluster
+ $server->configure_test_server_for_ssl($node, '127.0.0.1', '127.0.0.1/32', 'trust');
+
+=head1 DESCRIPTION
+
+SSL::Server configures an existing test cluster, for the SSL regression tests.
+
+The server is configured as follows:
+
+=over
+
+=item * SSL enabled, with the server certificate specified by arguments to switch_server_cert function.
+
+=item * reject non-SSL connections
+
+=item * a database called trustdb that lets anyone in
+
+=item * another database called certdb that uses certificate authentication, ie. the client must present a valid certificate signed by the client CA
+
+=back
+
+The server is configured to only accept connections from localhost. If you
+want to run the client from another host, you'll have to configure that
+manually.
+
+Note: Someone running these test could have key or certificate files in their
+~/.postgresql/, which would interfere with the tests. The way to override that
+is to specify sslcert=invalid and/or sslrootcert=invalid if no actual
+certificate is used for a particular test. libpq will ignore specifications
+that name nonexisting files. (sslkey and sslcrl do not need to specified
+explicitly because an invalid sslcert or sslrootcert, respectively, causes
+those to be ignored.)
+
+The SSL::Server module presents a SSL library abstraction to the test writer,
+which in turn use modules in SSL::Backend which implements the SSL library
+specific infrastructure. Currently only OpenSSL is supported.
+
+=cut
+
+package SSL::Server;
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use SSL::Backend::OpenSSL;
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item SSL::Server->new(flavor)
+
+Create a new SSL Server object for configuring a PostgreSQL test cluster
+node for accepting SSL connections using the with B<flavor> selected SSL
+backend. If B<flavor> isn't set, the C<with_ssl> environment variable will
+be used for selecting backend. Currently only C<openssl> is supported.
+
+=cut
+
+sub new
+{
+ my $class = shift;
+ my $flavor = shift || $ENV{with_ssl};
+ die "SSL flavor not defined" unless $flavor;
+ my $self = {};
+ bless $self, $class;
+ if ($flavor =~ /\Aopenssl\z/i)
+ {
+ $self->{flavor} = 'openssl';
+ $self->{backend} = SSL::Backend::OpenSSL->new();
+ }
+ else
+ {
+ die "SSL flavor $flavor unknown";
+ }
+ return $self;
+}
+
+=pod
+
+=item sslkey(filename)
+
+Return a C<sslkey> construct for the specified key for use in a connection
+string.
+
+=cut
+
+sub sslkey
+{
+ my $self = shift;
+ my $keyfile = shift;
+ my $backend = $self->{backend};
+
+ return $backend->get_sslkey($keyfile);
+}
+
+=pod
+
+=item $server->configure_test_server_for_ssl(node, host, cidr, auth, params)
+
+Configure the cluster specified by B<node> or listening on SSL connections.
+The following databases will be created in the cluster: trustdb, certdb,
+certdb_dn, certdb_dn_re, certdb_cn, verifydb. The following users will be
+created in the cluster: ssltestuser, md5testuser, anotheruser, yetanotheruser.
+If B<< $params{password} >> is set, it will be used as password for all users
+with the password encoding B<< $params{password_enc} >> (except for md5testuser
+which always have MD5). Extensions defined in B<< @{$params{extension}} >>
+will be created in all the above created databases. B<host> is used for
+C<listen_addresses> and B<cidr> for configuring C<pg_hba.conf>.
+
+=cut
+
+sub configure_test_server_for_ssl
+{
+ my $self=shift;
+ my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
+ my $backend = $self->{backend};
+ my $pgdata = $node->data_dir;
+
+ my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
+
+ # Create test users and databases
+ $node->psql('postgres', "CREATE USER ssltestuser");
+ $node->psql('postgres', "CREATE USER md5testuser");
+ $node->psql('postgres', "CREATE USER anotheruser");
+ $node->psql('postgres', "CREATE USER yetanotheruser");
+
+ foreach my $db (@databases)
+ {
+ $node->psql('postgres', "CREATE DATABASE $db");
+ }
+
+ # Update password of each user as needed.
+ if (defined($params{password}))
+ {
+ die "Password encryption must be specified when password is set"
+ unless defined($params{password_enc});
+
+ $node->psql('postgres',
+ "SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
+ );
+ # A special user that always has an md5-encrypted password
+ $node->psql('postgres',
+ "SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
+ );
+ $node->psql('postgres',
+ "SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
+ );
+ }
+
+ # Create any extensions requested in the setup
+ if (defined($params{extensions}))
+ {
+ foreach my $extension (@{$params{extensions}})
+ {
+ foreach my $db (@databases)
+ {
+ $node->psql($db, "CREATE EXTENSION $extension CASCADE;");
+ }
+ }
+ }
+
+ # enable logging etc.
+ open my $conf, '>>', "$pgdata/postgresql.conf";
+ print $conf "fsync=off\n";
+ print $conf "log_connections=on\n";
+ print $conf "log_hostname=on\n";
+ print $conf "listen_addresses='$serverhost'\n";
+ print $conf "log_statement=all\n";
+
+ # enable SSL and set up server key
+ print $conf "include 'sslconfig.conf'\n";
+
+ close $conf;
+
+ # SSL configuration will be placed here
+ open my $sslconf, '>', "$pgdata/sslconfig.conf";
+ close $sslconf;
+
+ # Perform backend specific configuration
+ $backend->init($pgdata);
+
+ # Stop and restart server to load new listen_addresses.
+ $node->restart;
+
+ # Change pg_hba after restart because hostssl requires ssl=on
+ _configure_hba_for_ssl($node, $servercidr, $authmethod);
+
+ return;
+}
+
+=pod
+
+=item $server->ssl_library()
+
+Get the name of the currently used SSL backend.
+
+=cut
+
+sub ssl_library
+{
+ my $self = shift;
+ my $backend = $self->{backend};
+
+ return $backend->get_library();
+}
+
+=pod
+
+=item switch_server_cert(params)
+
+Change the configuration to use the given set of certificate, key, ca and
+CRL, and potentially reload the configuration by restarting the server so
+that the configuration takes effect. Restarting is the default, passing
+B<< $params{restart} >> => 'no' opts out of it leaving the server running.
+The following params are supported:
+
+=over
+
+=item cafile => B<value>
+
+The CA certificate to use. Implementation is SSL backend specific.
+
+=item certfile => B<value>
+
+The certificate file to use. Implementation is SSL backend specific.
+
+=item keyfile => B<value>
+
+The private key to to use. Implementation is SSL backend specific.
+
+=item crlfile => B<value>
+
+The CRL file to use. Implementation is SSL backend specific.
+
+=item crldir => B<value>
+
+The CRL directory to use. Implementation is SSL backend specific.
+
+=item passphrase_cmd => B<value>
+
+The passphrase command to use. If not set, an empty passphrase command will
+be set.
+
+=item restart => B<value>
+
+If set to 'no', the server won't be restarted after updating the settings.
+If omitted, or any other value is passed, the server will be restarted before
+returning.
+
+=back
+
+=cut
+
+sub switch_server_cert
+{
+ my $self = shift;
+ my $node = shift;
+ my $backend = $self->{backend};
+ my %params = @_;
+ my $pgdata = $node->data_dir;
+
+ open my $sslconf, '>', "$pgdata/sslconfig.conf";
+ print $sslconf "ssl=on\n";
+ print $sslconf $backend->set_server_cert(\%params);
+ print $sslconf "ssl_passphrase_command='" . $params{passphrase_cmd} . "'\n"
+ if defined $params{passphrase_cmd};
+ close $sslconf;
+
+ return if (defined($params{restart}) && $params{restart} eq 'no');
+
+ $node->restart;
+ return;
+}
+
+
+# Internal function for configuring pg_hba.conf for SSL connections.
+sub _configure_hba_for_ssl
+{
+ my ($node, $servercidr, $authmethod) = @_;
+ my $pgdata = $node->data_dir;
+
+ # Only accept SSL connections from $servercidr. Our tests don't depend on this
+ # but seems best to keep it as narrow as possible for security reasons.
+ #
+ # When connecting to certdb, also check the client certificate.
+ open my $hba, '>', "$pgdata/pg_hba.conf";
+ print $hba
+ "# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n";
+ print $hba
+ "hostssl trustdb md5testuser $servercidr md5\n";
+ print $hba
+ "hostssl trustdb all $servercidr $authmethod\n";
+ print $hba
+ "hostssl verifydb ssltestuser $servercidr $authmethod clientcert=verify-full\n";
+ print $hba
+ "hostssl verifydb anotheruser $servercidr $authmethod clientcert=verify-full\n";
+ print $hba
+ "hostssl verifydb yetanotheruser $servercidr $authmethod clientcert=verify-ca\n";
+ print $hba
+ "hostssl certdb all $servercidr cert\n";
+ print $hba
+ "hostssl certdb_dn all $servercidr cert clientname=DN map=dn\n",
+ "hostssl certdb_dn_re all $servercidr cert clientname=DN map=dnre\n",
+ "hostssl certdb_cn all $servercidr cert clientname=CN map=cn\n";
+ close $hba;
+
+ # Also set the ident maps. Note: fields with commas must be quoted
+ open my $map, ">", "$pgdata/pg_ident.conf";
+ print $map
+ "# MAPNAME SYSTEM-USERNAME PG-USERNAME\n",
+ "dn \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\" ssltestuser\n",
+ "dnre \"/^.*OU=Testing,.*\$\" ssltestuser\n",
+ "cn ssltestuser-dn ssltestuser\n";
+
+ return;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm
deleted file mode 100644
index c85c6fd997..0000000000
--- a/src/test/ssl/t/SSLServer.pm
+++ /dev/null
@@ -1,219 +0,0 @@
-
-# Copyright (c) 2021-2022, PostgreSQL Global Development Group
-
-# This module sets up a test server, for the SSL regression tests.
-#
-# The server is configured as follows:
-#
-# - SSL enabled, with the server certificate specified by argument to
-# switch_server_cert function.
-# - ssl/root+client_ca.crt as the CA root for validating client certs.
-# - reject non-SSL connections
-# - a database called trustdb that lets anyone in
-# - another database called certdb that uses certificate authentication, ie.
-# the client must present a valid certificate signed by the client CA
-#
-# The server is configured to only accept connections from localhost. If you
-# want to run the client from another host, you'll have to configure that
-# manually.
-#
-# Note: Someone running these test could have key or certificate files
-# in their ~/.postgresql/, which would interfere with the tests. The
-# way to override that is to specify sslcert=invalid and/or
-# sslrootcert=invalid if no actual certificate is used for a
-# particular test. libpq will ignore specifications that name
-# nonexisting files. (sslkey and sslcrl do not need to specified
-# explicitly because an invalid sslcert or sslrootcert, respectively,
-# causes those to be ignored.)
-
-package SSLServer;
-
-use strict;
-use warnings;
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use File::Basename;
-use File::Copy;
-use Test::More;
-
-use Exporter 'import';
-our @EXPORT = qw(
- configure_test_server_for_ssl
- switch_server_cert
-);
-
-# Copy a set of files, taking into account wildcards
-sub copy_files
-{
- my $orig = shift;
- my $dest = shift;
-
- my @orig_files = glob $orig;
- foreach my $orig_file (@orig_files)
- {
- my $base_file = basename($orig_file);
- copy($orig_file, "$dest/$base_file")
- or die "Could not copy $orig_file to $dest";
- }
- return;
-}
-
-# serverhost: what to put in listen_addresses, e.g. '127.0.0.1'
-# servercidr: what to put in pg_hba.conf, e.g. '127.0.0.1/32'
-sub configure_test_server_for_ssl
-{
- my ($node, $serverhost, $servercidr, $authmethod, %params) = @_;
- my $pgdata = $node->data_dir;
-
- my @databases = ( 'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re', 'certdb_cn', 'verifydb' );
-
- # Create test users and databases
- $node->psql('postgres', "CREATE USER ssltestuser");
- $node->psql('postgres', "CREATE USER md5testuser");
- $node->psql('postgres', "CREATE USER anotheruser");
- $node->psql('postgres', "CREATE USER yetanotheruser");
-
- foreach my $db (@databases)
- {
- $node->psql('postgres', "CREATE DATABASE $db");
- }
-
- # Update password of each user as needed.
- if (defined($params{password}))
- {
- die "Password encryption must be specified when password is set"
- unless defined($params{password_enc});
-
- $node->psql('postgres',
- "SET password_encryption='$params{password_enc}'; ALTER USER ssltestuser PASSWORD '$params{password}';"
- );
- # A special user that always has an md5-encrypted password
- $node->psql('postgres',
- "SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$params{password}';"
- );
- $node->psql('postgres',
- "SET password_encryption='$params{password_enc}'; ALTER USER anotheruser PASSWORD '$params{password}';"
- );
- }
-
- # Create any extensions requested in the setup
- if (defined($params{extensions}))
- {
- foreach my $extension (@{$params{extensions}})
- {
- foreach my $db (@databases)
- {
- $node->psql($db, "CREATE EXTENSION $extension CASCADE;");
- }
- }
- }
-
- # enable logging etc.
- open my $conf, '>>', "$pgdata/postgresql.conf";
- print $conf "fsync=off\n";
- print $conf "log_connections=on\n";
- print $conf "log_hostname=on\n";
- print $conf "listen_addresses='$serverhost'\n";
- print $conf "log_statement=all\n";
-
- # enable SSL and set up server key
- print $conf "include 'sslconfig.conf'\n";
-
- close $conf;
-
- # ssl configuration will be placed here
- open my $sslconf, '>', "$pgdata/sslconfig.conf";
- close $sslconf;
-
- # Copy all server certificates and keys, and client root cert, to the data dir
- copy_files("ssl/server-*.crt", $pgdata);
- copy_files("ssl/server-*.key", $pgdata);
- chmod(0600, glob "$pgdata/server-*.key") or die $!;
- copy_files("ssl/root+client_ca.crt", $pgdata);
- copy_files("ssl/root_ca.crt", $pgdata);
- copy_files("ssl/root+client.crl", $pgdata);
- mkdir("$pgdata/root+client-crldir");
- copy_files("ssl/root+client-crldir/*", "$pgdata/root+client-crldir/");
-
- # Stop and restart server to load new listen_addresses.
- $node->restart;
-
- # Change pg_hba after restart because hostssl requires ssl=on
- configure_hba_for_ssl($node, $servercidr, $authmethod);
-
- return;
-}
-
-# Change the configuration to use given server cert file, and reload
-# the server so that the configuration takes effect.
-sub switch_server_cert
-{
- my $node = $_[0];
- my $certfile = $_[1];
- my $cafile = $_[2] || "root+client_ca";
- my $crlfile = "root+client.crl";
- my $crldir;
- my $pgdata = $node->data_dir;
-
- # defaults to use crl file
- if (defined $_[3] || defined $_[4])
- {
- $crlfile = $_[3];
- $crldir = $_[4];
- }
-
- open my $sslconf, '>', "$pgdata/sslconfig.conf";
- print $sslconf "ssl=on\n";
- print $sslconf "ssl_ca_file='$cafile.crt'\n";
- print $sslconf "ssl_cert_file='$certfile.crt'\n";
- print $sslconf "ssl_key_file='$certfile.key'\n";
- print $sslconf "ssl_crl_file='$crlfile'\n" if defined $crlfile;
- print $sslconf "ssl_crl_dir='$crldir'\n" if defined $crldir;
- close $sslconf;
-
- $node->restart;
- return;
-}
-
-sub configure_hba_for_ssl
-{
- my ($node, $servercidr, $authmethod) = @_;
- my $pgdata = $node->data_dir;
-
- # Only accept SSL connections from $servercidr. Our tests don't depend on this
- # but seems best to keep it as narrow as possible for security reasons.
- #
- # When connecting to certdb, also check the client certificate.
- open my $hba, '>', "$pgdata/pg_hba.conf";
- print $hba
- "# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n";
- print $hba
- "hostssl trustdb md5testuser $servercidr md5\n";
- print $hba
- "hostssl trustdb all $servercidr $authmethod\n";
- print $hba
- "hostssl verifydb ssltestuser $servercidr $authmethod clientcert=verify-full\n";
- print $hba
- "hostssl verifydb anotheruser $servercidr $authmethod clientcert=verify-full\n";
- print $hba
- "hostssl verifydb yetanotheruser $servercidr $authmethod clientcert=verify-ca\n";
- print $hba
- "hostssl certdb all $servercidr cert\n";
- print $hba
- "hostssl certdb_dn all $servercidr cert clientname=DN map=dn\n",
- "hostssl certdb_dn_re all $servercidr cert clientname=DN map=dnre\n",
- "hostssl certdb_cn all $servercidr cert clientname=CN map=cn\n";
- close $hba;
-
- # Also set the ident maps. Note: fields with commas must be quoted
- open my $map, ">", "$pgdata/pg_ident.conf";
- print $map
- "# MAPNAME SYSTEM-USERNAME PG-USERNAME\n",
- "dn \"CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG\" ssltestuser\n",
- "dnre \"/^.*OU=Testing,.*\$\" ssltestuser\n",
- "cn ssltestuser-dn ssltestuser\n";
-
- return;
-}
-
-1;
--
2.24.3 (Apple Git-128)
^ permalink raw reply [nested|flat] 74+ messages in thread
* Re: Refactoring SSL tests
@ 2022-02-09 13:28 Andrew Dunstan <[email protected]>
parent: Daniel Gustafsson <[email protected]>
0 siblings, 1 reply; 74+ messages in thread
From: Andrew Dunstan @ 2022-02-09 13:28 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On 2/9/22 08:11, Daniel Gustafsson wrote:
>> On 8 Feb 2022, at 16:46, Andrew Dunstan <[email protected]> wrote:
>> There a capitalization typo in SSL/Backend/OpenSSL.pm - looks like
>> that's my fault:
>>
>> + my $backend = SSL::backend::OpenSSL->new();
> Fixed.
>
>> Also, I think we should document that SSL::Server::new() takes an
>> optional flavor parameter, in the absence of which it uses
>> $ENV{with_ssl}, and that 'openssl' is the only currently supported value.
> Good point, done in the attached.
LGTM
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 74+ messages in thread
* Re: Refactoring SSL tests
@ 2022-03-26 21:08 Daniel Gustafsson <[email protected]>
parent: Andrew Dunstan <[email protected]>
0 siblings, 0 replies; 74+ messages in thread
From: Daniel Gustafsson @ 2022-03-26 21:08 UTC (permalink / raw)
To: Andrew Dunstan <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
> On 9 Feb 2022, at 14:28, Andrew Dunstan <[email protected]> wrote:
> On 2/9/22 08:11, Daniel Gustafsson wrote:
>> Good point, done in the attached.
>
> LGTM
Now that the recent changes to TAP and SSL tests have settled, I took another
pass at this. After rebasing and fixing and polishing and taking it for
multiple spins on the CI I've pushed this today to master.
--
Daniel Gustafsson https://vmware.com/
^ permalink raw reply [nested|flat] 74+ messages in thread
end of thread, other threads:[~2022-03-26 21:08 UTC | newest]
Thread overview: 74+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2021-02-02 00:57 [PATCH 7/9] Remove the special batch mode, use a larger buffer always Tomas Vondra <[email protected]>
2022-02-02 16:09 Re: Refactoring SSL tests Andrew Dunstan <[email protected]>
2022-02-02 19:50 ` Re: Refactoring SSL tests Daniel Gustafsson <[email protected]>
2022-02-07 16:29 ` Re: Refactoring SSL tests Andrew Dunstan <[email protected]>
2022-02-08 14:24 ` Re: Refactoring SSL tests Daniel Gustafsson <[email protected]>
2022-02-08 15:46 ` Re: Refactoring SSL tests Andrew Dunstan <[email protected]>
2022-02-09 13:11 ` Re: Refactoring SSL tests Daniel Gustafsson <[email protected]>
2022-02-09 13:28 ` Re: Refactoring SSL tests Andrew Dunstan <[email protected]>
2022-03-26 21:08 ` Re: Refactoring SSL tests Daniel Gustafsson <[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