($INBOX_DIR/description missing)  
help / color / mirror / Atom feed
[PATCH 7/9] Remove the special batch mode, use a larger buffer always
72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ 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; 72+ 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] 72+ messages in thread

* Re: Can we let extensions change their dumped catalog schemas?
@ 2023-01-11 03:53  Tom Lane <[email protected]>
  0 siblings, 1 reply; 72+ messages in thread

From: Tom Lane @ 2023-01-11 03:53 UTC (permalink / raw)
  To: Jacob Champion <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

Jacob Champion <[email protected]> writes:
> We'd like to be allowed to change the schema for a table that's been
> marked in the past with pg_extension_config_dump().

> Unless I'm missing something obvious (please, let it be that) there's no
> way to do this safely. Once you've marked an internal table as dumpable,
> its schema is effectively frozen if you want your dumps to work across
> versions, because otherwise you'll try to restore that "catalog" data
> into a table that has different columns. And while sometimes you can
> make that work, it doesn't in the general case.

I agree that's a problem, but it's not that we're arbitrarily prohibiting
something that would work.  What, concretely, do you think could be
done to improve the situation?

> 2) Provide a way to record the exact version of an extension in a dump.

Don't really see how that helps?  I also fear that it will break
a bunch of use-cases that work fine today, which are exactly the
ones for which we originally defined pg_dump as *not* committing
to a particular extension version.

It feels like what we really need here is some way to mutate the
old format of an extension config table into the new format.
Simple addition of new columns shouldn't be a problem (in fact,
I think that works already, or could easily be made to).  If you
want some ETL processing then it's harder :-(.  Could an ON INSERT
trigger on an old config table transpose converted data into a
newer config table?

Another point that ought to be made here is that pg_dump is not
the only outside consumer of extension config data.  You're likely
to break some applications if you change a config table too much.
That's not an argument that we shouldn't try to make pg_dump more
forgiving, but I'm not sure that we need to move heaven and earth.

			regards, tom lane






^ permalink  raw  reply  [nested|flat] 72+ messages in thread

* Re: Can we let extensions change their dumped catalog schemas?
@ 2023-01-11 18:27  Jacob Champion <[email protected]>
  parent: Tom Lane <[email protected]>
  0 siblings, 1 reply; 72+ messages in thread

From: Jacob Champion @ 2023-01-11 18:27 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On Tue, Jan 10, 2023 at 7:53 PM Tom Lane <[email protected]> wrote:
> Jacob Champion <[email protected]> writes:
> > Unless I'm missing something obvious (please, let it be that) there's no
> > way to do this safely. Once you've marked an internal table as dumpable,
> > its schema is effectively frozen if you want your dumps to work across
> > versions, because otherwise you'll try to restore that "catalog" data
> > into a table that has different columns. And while sometimes you can
> > make that work, it doesn't in the general case.
>
> I agree that's a problem, but it's not that we're arbitrarily prohibiting
> something that would work.  What, concretely, do you think could be
> done to improve the situation?

Concretely, I think extensions should be able to invoke their update
scripts at some point after a dump/restore cycle, whether
automatically or manually.

> > 2) Provide a way to record the exact version of an extension in a dump.
>
> Don't really see how that helps?

If pg_dump recorded our extension using

    CREATE EXTENSION timescaledb VERSION <original version>

then we'd be able to migrate the changed catalog post-restore, using a
standard ALTER EXTENSION ... UPDATE.

> I also fear that it will break
> a bunch of use-cases that work fine today, which are exactly the
> ones for which we originally defined pg_dump as *not* committing
> to a particular extension version.

Right, I think it would have to be opt-in. Say, a new control file
option dump_version or some such.

> It feels like what we really need here is some way to mutate the
> old format of an extension config table into the new format.

Agreed. We already provide mutation functions via the update scripts,
so I think both proposal 2 and 3 do that. I'm curious about your
opinion on option 3, since it would naively seem to make pg_dump do
_less_ work for a given extension.

> Simple addition of new columns shouldn't be a problem (in fact,
> I think that works already, or could easily be made to).  If you
> want some ETL processing then it's harder :-(.

One sharp edge for the add-a-new-column case is where you give the new
column a default, and you want all of the old migrated rows to have
some non-default value to handle backwards compatibility. (But that
case is handled trivially if you _know_ that you're performing a
migration.)

> Could an ON INSERT
> trigger on an old config table transpose converted data into a
> newer config table?

You mean something like, introduce table catalog_v2, and have all
INSERTs to catalog_v1 migrate and redirect the rows? That seems like
it could work today, though it would mean maintaining two different
upgrade paths for the same table, migrating all users of the catalog
to the new name, and needing to drop the old table at... some point
after the restore? I don't know if there would be performance concerns
with larger catalogs (in fact I'm not sure how big these catalogs
get).

> Another point that ought to be made here is that pg_dump is not
> the only outside consumer of extension config data.  You're likely
> to break some applications if you change a config table too much.

Such as? We don't really want applications to be coupled against our
internals by accident, but we have to dump the internals to be able to
reproduce the state of the system.

> That's not an argument that we shouldn't try to make pg_dump more
> forgiving, but I'm not sure that we need to move heaven and earth.

Agreed. Hopefully we can find something that just moves a little earth. :D

Thanks!
--Jacob






^ permalink  raw  reply  [nested|flat] 72+ messages in thread

* Re: Can we let extensions change their dumped catalog schemas?
@ 2023-01-11 21:03  Tom Lane <[email protected]>
  parent: Jacob Champion <[email protected]>
  0 siblings, 1 reply; 72+ messages in thread

From: Tom Lane @ 2023-01-11 21:03 UTC (permalink / raw)
  To: Jacob Champion <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

Jacob Champion <[email protected]> writes:
> On Tue, Jan 10, 2023 at 7:53 PM Tom Lane <[email protected]> wrote:
>> I also fear that it will break
>> a bunch of use-cases that work fine today, which are exactly the
>> ones for which we originally defined pg_dump as *not* committing
>> to a particular extension version.

> Right, I think it would have to be opt-in. Say, a new control file
> option dump_version or some such.

That would require all the installed extensions to cope with this
the same way, which does not seem like a great assumption.

			regards, tom lane






^ permalink  raw  reply  [nested|flat] 72+ messages in thread

* Re: Can we let extensions change their dumped catalog schemas?
@ 2023-01-12 19:04  Jacob Champion <[email protected]>
  parent: Tom Lane <[email protected]>
  0 siblings, 1 reply; 72+ messages in thread

From: Jacob Champion @ 2023-01-12 19:04 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On Wed, Jan 11, 2023 at 1:03 PM Tom Lane <[email protected]> wrote:
> Jacob Champion <[email protected]> writes:
> > Right, I think it would have to be opt-in. Say, a new control file
> > option dump_version or some such.
>
> That would require all the installed extensions to cope with this
> the same way, which does not seem like a great assumption.

How so? Most installed extensions would not opt into a version dump,
I'd imagine.

Or do you mean that the version dump would apply retroactively to
older versions of the extension, even if it wasn't needed in the past?

--Jacob






^ permalink  raw  reply  [nested|flat] 72+ messages in thread

* Re: Can we let extensions change their dumped catalog schemas?
@ 2023-01-17 23:18  Jacob Champion <[email protected]>
  parent: Jacob Champion <[email protected]>
  0 siblings, 1 reply; 72+ messages in thread

From: Jacob Champion @ 2023-01-17 23:18 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On 1/12/23 11:04, Jacob Champion wrote:
> On Wed, Jan 11, 2023 at 1:03 PM Tom Lane <[email protected]> wrote:
>> Jacob Champion <[email protected]> writes:
>>> Right, I think it would have to be opt-in. Say, a new control file
>>> option dump_version or some such.
>>
>> That would require all the installed extensions to cope with this
>> the same way, which does not seem like a great assumption.
> 
> How so? Most installed extensions would not opt into a version dump,
> I'd imagine.

As a concrete example, Timescale's extension control file could look
like this:

    default_version = '2.x.y'
    module_pathname = '$libdir/timescaledb-2.x.y'
    ...
    dump_version = true

which would then cause pg_dump to issue a VERSION for its CREATE
EXTENSION line. Other extensions would remain with the default
(dump_version = false), so they'd be dumped without an explicit VERSION.

(And in the case of option 3, the name of the control file option
changes -- dump_internals, maybe? -- but it still doesn't affect other
installed extensions.)

--Jacob






^ permalink  raw  reply  [nested|flat] 72+ messages in thread

* Re: Can we let extensions change their dumped catalog schemas?
@ 2023-02-07 18:16  Jacob Champion <[email protected]>
  parent: Jacob Champion <[email protected]>
  0 siblings, 0 replies; 72+ messages in thread

From: Jacob Champion @ 2023-02-07 18:16 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On Tue, Jan 17, 2023 at 3:18 PM Jacob Champion <[email protected]> wrote:
> As a concrete example, Timescale's extension control file could look
> like this:
>
>     default_version = '2.x.y'
>     module_pathname = '$libdir/timescaledb-2.x.y'
>     ...
>     dump_version = true
>
> which would then cause pg_dump to issue a VERSION for its CREATE
> EXTENSION line. Other extensions would remain with the default
> (dump_version = false), so they'd be dumped without an explicit VERSION.

Even more concretely, here's a draft patch. There's no nuance --
setting dump_version affects all past versions of the extension, and
it can't be turned off at restore time. So extensions opting in would
have to be written to be side-by-side installable. (Ours is, in its
own way, but the PGDG installers don't allow it -- which maybe
highlights a weakness in this approach.) I'm still not sure if this
addresses Tom's concerns, or just adds new ones.

We could maybe give the user more control by overriding the default
version for an extension in a different TOC entry, and then add
options to ignore or include those version numbers during restore.
That doesn't address the side-by-side problem directly but gives an
escape hatch.

Earlier I wrote:

> I'm curious about your
> opinion on option 3, since it would naively seem to make pg_dump do
> _less_ work for a given extension.

This was definitely naive :D We can't just make use of the
binary-upgrade machinery to dump extension internals, because it pins
OIDs. So that might still be a valid approach, but it's not "less
work."

Thanks,
--Jacob


Attachments:

  [text/x-patch] WIP-implement-dump_version-control-option.patch (6.7K, ../../CAAWbhmgwCWR1GNQaYOPP=uCX2E_ZU9oPHaJhBvyUksnfgdF4bg@mail.gmail.com/2-WIP-implement-dump_version-control-option.patch)
  download | inline diff:
From 1a79eacbc6bdad8bb5e05418756d6ab35e9bab9e Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Fri, 27 Jan 2023 10:42:43 -0800
Subject: [PATCH] WIP: implement dump_version control option

The intent is to allow extensions to tie their internal catalog version
to a dump. This allows any dumped catalog tables to be upgraded after a
restore. The downside is that both the old and new extension versions
must be available for installation on the target.

This passes manual sanity checks, but there is no test yet.
---
 src/backend/commands/extension.c | 29 +++++++++++++++++++++++++++++
 src/bin/pg_dump/pg_dump.c        | 25 ++++++++++++++++++++++---
 src/bin/pg_dump/pg_dump.h        |  1 +
 src/include/catalog/pg_proc.dat  |  4 ++++
 4 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index b1509cc505..b58c1d27a1 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -88,6 +88,7 @@ typedef struct ExtensionControlFile
 	bool		relocatable;	/* is ALTER EXTENSION SET SCHEMA supported? */
 	bool		superuser;		/* must be superuser to install? */
 	bool		trusted;		/* allow becoming superuser on the fly? */
+	bool		dump_version;	/* record the VERSION during pg_dump? */
 	int			encoding;		/* encoding of the script file, or -1 */
 	List	   *requires;		/* names of prerequisite extensions */
 } ExtensionControlFile;
@@ -582,6 +583,14 @@ parse_extension_control_file(ExtensionControlFile *control,
 						 errmsg("parameter \"%s\" requires a Boolean value",
 								item->name)));
 		}
+		else if (strcmp(item->name, "dump_version") == 0)
+		{
+			if (!parse_bool(item->value, &control->dump_version))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("parameter \"%s\" requires a Boolean value",
+								item->name)));
+		}
 		else if (strcmp(item->name, "encoding") == 0)
 		{
 			control->encoding = pg_valid_server_encoding(item->value);
@@ -639,6 +648,7 @@ read_extension_control_file(const char *extname)
 	control->relocatable = false;
 	control->superuser = true;
 	control->trusted = false;
+	control->dump_version = false;
 	control->encoding = -1;
 
 	/*
@@ -2532,6 +2542,25 @@ pg_extension_config_dump(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/*
+ * This function reports whether an extension's version should be written
+ * explicitly by pg_dump.
+ */
+Datum
+pg_extension_version_is_dumpable(PG_FUNCTION_ARGS)
+{
+	Name		extname = PG_GETARG_NAME(0);
+	ExtensionControlFile *control;
+
+	/* Check extension name validity before any filesystem access */
+	check_valid_extension_name(NameStr(*extname));
+
+	/* Read the extension's control file */
+	control = read_extension_control_file(NameStr(*extname));
+
+	PG_RETURN_BOOL(control->dump_version);
+}
+
 /*
  * extension_config_remove
  *
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 527c7651ab..177ffbede5 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5209,11 +5209,21 @@ getExtensions(Archive *fout, int *numExtensions)
 	int			i_extversion;
 	int			i_extconfig;
 	int			i_extcondition;
+	int			i_dump_version;
 
 	query = createPQExpBuffer();
 
 	appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
-						 "x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
+						 "x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition, ");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBufferStr(query,
+							 "pg_catalog.pg_extension_version_is_dumpable(x.extname) AS dump_version ");
+	else
+		appendPQExpBufferStr(query,
+							 "false AS dump_version ");
+
+	appendPQExpBufferStr(query,
 						 "FROM pg_extension x "
 						 "JOIN pg_namespace n ON n.oid = x.extnamespace");
 
@@ -5231,6 +5241,7 @@ getExtensions(Archive *fout, int *numExtensions)
 	i_extversion = PQfnumber(res, "extversion");
 	i_extconfig = PQfnumber(res, "extconfig");
 	i_extcondition = PQfnumber(res, "extcondition");
+	i_dump_version = PQfnumber(res, "dump_version");
 
 	for (i = 0; i < ntups; i++)
 	{
@@ -5244,6 +5255,7 @@ getExtensions(Archive *fout, int *numExtensions)
 		extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
 		extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
 		extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
+		extinfo[i].dump_version = *(PQgetvalue(res, i, i_dump_version)) == 't';
 
 		/* Decide whether we want to dump it */
 		selectDumpableExtension(&(extinfo[i]), dopt);
@@ -10172,15 +10184,22 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 		/*
 		 * In a regular dump, we simply create the extension, intentionally
 		 * not specifying a version, so that the destination installation's
-		 * default version is used.
+		 * default version is used. Extensions may opt into having their version
+		 * dumped explicitly using their control file.
 		 *
 		 * Use of IF NOT EXISTS here is unlike our behavior for other object
 		 * types; but there are various scenarios in which it's convenient to
 		 * manually create the desired extension before restoring, so we
 		 * prefer to allow it to exist already.
 		 */
-		appendPQExpBuffer(q, "CREATE EXTENSION IF NOT EXISTS %s WITH SCHEMA %s;\n",
+		appendPQExpBuffer(q, "CREATE EXTENSION IF NOT EXISTS %s WITH SCHEMA %s",
 						  qextname, fmtId(extinfo->namespace));
+		if (extinfo->dump_version)
+		{
+			appendPQExpBufferStr(q, " VERSION ");
+			appendStringLiteralAH(q, extinfo->extversion, fout);
+		}
+		appendPQExpBufferStr(q, ";\n");
 	}
 	else
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index e7cbd8d7ed..bef4633b97 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -180,6 +180,7 @@ typedef struct _extensionInfo
 	DumpableObject dobj;
 	char	   *namespace;		/* schema containing extension's objects */
 	bool		relocatable;
+	bool		dump_version;
 	char	   *extversion;
 	char	   *extconfig;		/* info about configuration tables */
 	char	   *extcondition;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index c0f2a8a77c..4e0a6377b2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10211,6 +10211,10 @@
   proname => 'pg_extension_config_dump', provolatile => 'v', proparallel => 'u',
   prorettype => 'void', proargtypes => 'regclass text',
   prosrc => 'pg_extension_config_dump' },
+{ oid => '9043', descr => 'should pg_dump emit this extension\'s version?',
+  proname => 'pg_extension_version_is_dumpable', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name',
+  prosrc => 'pg_extension_version_is_dumpable' },
 
 # SQL-spec window functions
 { oid => '3100', descr => 'row number within partition',
-- 
2.25.1



^ permalink  raw  reply  [nested|flat] 72+ messages in thread


end of thread, other threads:[~2023-02-07 18:16 UTC | newest]

Thread overview: 72+ 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]>
2023-01-11 03:53 Re: Can we let extensions change their dumped catalog schemas? Tom Lane <[email protected]>
2023-01-11 18:27 ` Re: Can we let extensions change their dumped catalog schemas? Jacob Champion <[email protected]>
2023-01-11 21:03   ` Re: Can we let extensions change their dumped catalog schemas? Tom Lane <[email protected]>
2023-01-12 19:04     ` Re: Can we let extensions change their dumped catalog schemas? Jacob Champion <[email protected]>
2023-01-17 23:18       ` Re: Can we let extensions change their dumped catalog schemas? Jacob Champion <[email protected]>
2023-02-07 18:16         ` Re: Can we let extensions change their dumped catalog schemas? Jacob Champion <[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