public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH 7/9] Remove the special batch mode, use a larger buffer always
84+ messages / 5 participants
[nested] [flat]

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* [PATCH 7/9] Remove the special batch mode, use a larger buffer always
@ 2021-02-02 00:57 Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Tomas Vondra @ 2021-02-02 00:57 UTC (permalink / raw)

Instead of using a batch mode (with a larger input buffer) only for new
ranges, which introduces "special cases" in various places, use it as
the standard approach.

Also, instead of sizing the buffer to cover the whole range, limit it
to some reasonable limit (10x the user-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] 84+ messages in thread

* sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
@ 2025-02-03 12:05 Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-02-03 12:05 UTC (permalink / raw)
  To: [email protected] <[email protected]>

Good day,

Investigating some performance issues of a client, our engineers found
msgnumLock to be contended.

Looking closer it is obvious it is not needed at all: it used only as
memory barrier. It is even stated in comment at file start:

* We deal with that by having a spinlock that readers must take for just
* long enough to read maxMsgNum, while writers take it for just long enough
* to write maxMsgNum.  (The exact rule is that you need the spinlock to
* read maxMsgNum if you are not holding SInvalWriteLock, and you need the
* spinlock to write maxMsgNum unless you are holding both locks.)
*
* Note: since maxMsgNum is an int and hence presumably atomically readable/
* writable, the spinlock might seem unnecessary.  The reason it is needed
* is to provide a memory barrier: we need to be sure that messages written
* to the array are actually there before maxMsgNum is increased, and that
* readers will see that data after fetching maxMsgNum.

So we changed maxMsgNum to be pg_atomic_uint32, and put appropriate memory
barriers:
- pg_write_barrier() before write to maxMsgNum (when only SInvalWriteLock
is held)
- pg_read_barrier() after read of maxMsgNum (when only SInvalReadLock is held)

It improved situation for our client.

Note: pg_(write|read)_barrier() is chosen instead of
pg_atomic_(read|write)_membarrier_u32() because it certainly cheaper at
least on x86_64 where it is translated to just a compiler barrier (empty
asm statement).
At least pg_atomic_read_membarrier_u32() is implemented currently as a
write operation, that's not good for contended place.

-----

regards
Yura Sokolov aka funny-falcon

Attachments:

  [text/x-patch] v0-0001-sinvaladt.c-use-atomic-operations-on-maxMsgNum.patch (6.3K, ../../[email protected]/2-v0-0001-sinvaladt.c-use-atomic-operations-on-maxMsgNum.patch)
  download | inline diff:
From 351b3ecb5d085559beb13ffd6dd4416dab4b4a84 Mon Sep 17 00:00:00 2001
From: Yura Sokolov <[email protected]>
Date: Mon, 3 Feb 2025 11:58:33 +0300
Subject: [PATCH v0] sinvaladt.c: use atomic operations on maxMsgNum

msgnumLock spinlock could be highly contended.
Comment states it was used as memory barrier.
Lets use atomic ops with memory barriers directly instead.

Note: patch uses pg_read_barrier()/pg_write_barrier() instead of
pg_atomic_read_membarrier_u32()/pg_atomic_write_membarrier_u32() since
no full barrier semantic is required, and explicit read/write barriers
are cheaper at least on x86_64.
---
 src/backend/storage/ipc/sinvaladt.c | 56 ++++++++++++-----------------
 1 file changed, 23 insertions(+), 33 deletions(-)

diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.c
index 2da91738c32..7d4912c6419 100644
--- a/src/backend/storage/ipc/sinvaladt.c
+++ b/src/backend/storage/ipc/sinvaladt.c
@@ -86,19 +86,10 @@
  * has no need to touch anyone's ProcState, except in the infrequent cases
  * when SICleanupQueue is needed.  The only point of overlap is that
  * the writer wants to change maxMsgNum while readers need to read it.
- * We deal with that by having a spinlock that readers must take for just
- * long enough to read maxMsgNum, while writers take it for just long enough
- * to write maxMsgNum.  (The exact rule is that you need the spinlock to
- * read maxMsgNum if you are not holding SInvalWriteLock, and you need the
- * spinlock to write maxMsgNum unless you are holding both locks.)
- *
- * Note: since maxMsgNum is an int and hence presumably atomically readable/
- * writable, the spinlock might seem unnecessary.  The reason it is needed
- * is to provide a memory barrier: we need to be sure that messages written
- * to the array are actually there before maxMsgNum is increased, and that
- * readers will see that data after fetching maxMsgNum.  Multiprocessors
- * that have weak memory-ordering guarantees can fail without the memory
- * barrier instructions that are included in the spinlock sequences.
+ * We deal with that by using atomic operations with proper memory barriers.
+ * (The exact rule is that you need the read barrier after atomic read
+ * maxMsgNum if you are not holding SInvalWriteLock, and you need the
+ * write barrier before write maxMsgNum unless you are holding both locks.)
  */
 
 
@@ -168,11 +159,9 @@ typedef struct SISeg
 	 * General state information
 	 */
 	int			minMsgNum;		/* oldest message still needed */
-	int			maxMsgNum;		/* next message number to be assigned */
+	pg_atomic_uint32 maxMsgNum; /* next message number to be assigned */
 	int			nextThreshold;	/* # of messages to call SICleanupQueue */
 
-	slock_t		msgnumLock;		/* spinlock protecting maxMsgNum */
-
 	/*
 	 * Circular buffer holding shared-inval messages
 	 */
@@ -243,9 +232,8 @@ SharedInvalShmemInit(void)
 
 	/* Clear message counters, save size of procState array, init spinlock */
 	shmInvalBuffer->minMsgNum = 0;
-	shmInvalBuffer->maxMsgNum = 0;
+	pg_atomic_init_u32(&shmInvalBuffer->maxMsgNum, 0);
 	shmInvalBuffer->nextThreshold = CLEANUP_MIN;
-	SpinLockInit(&shmInvalBuffer->msgnumLock);
 
 	/* The buffer[] array is initially all unused, so we need not fill it */
 
@@ -303,7 +291,7 @@ SharedInvalBackendInit(bool sendOnly)
 
 	/* mark myself active, with all extant messages already read */
 	stateP->procPid = MyProcPid;
-	stateP->nextMsgNum = segP->maxMsgNum;
+	stateP->nextMsgNum = pg_atomic_read_u32(&segP->maxMsgNum);
 	stateP->resetState = false;
 	stateP->signaled = false;
 	stateP->hasMessages = false;
@@ -399,7 +387,7 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		 */
 		for (;;)
 		{
-			numMsgs = segP->maxMsgNum - segP->minMsgNum;
+			numMsgs = pg_atomic_read_u32(&segP->maxMsgNum) - segP->minMsgNum;
 			if (numMsgs + nthistime > MAXNUMMESSAGES ||
 				numMsgs >= segP->nextThreshold)
 				SICleanupQueue(true, nthistime);
@@ -410,17 +398,16 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		/*
 		 * Insert new message(s) into proper slot of circular buffer
 		 */
-		max = segP->maxMsgNum;
+		max = pg_atomic_read_u32(&segP->maxMsgNum);
 		while (nthistime-- > 0)
 		{
 			segP->buffer[max % MAXNUMMESSAGES] = *data++;
 			max++;
 		}
 
-		/* Update current value of maxMsgNum using spinlock */
-		SpinLockAcquire(&segP->msgnumLock);
-		segP->maxMsgNum = max;
-		SpinLockRelease(&segP->msgnumLock);
+		/* Update current value of maxMsgNum using atomic with memory barrier */
+		pg_write_barrier();
+		pg_atomic_write_u32(&segP->maxMsgNum, max);
 
 		/*
 		 * Now that the maxMsgNum change is globally visible, we give everyone
@@ -506,10 +493,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 	 */
 	stateP->hasMessages = false;
 
-	/* Fetch current value of maxMsgNum using spinlock */
-	SpinLockAcquire(&segP->msgnumLock);
-	max = segP->maxMsgNum;
-	SpinLockRelease(&segP->msgnumLock);
+	/* Fetch current value of maxMsgNum using atomic with memory barrier */
+	max = pg_atomic_read_u32(&segP->maxMsgNum);
+	pg_read_barrier();
 
 	if (stateP->resetState)
 	{
@@ -595,7 +581,7 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 * backends here it is possible for them to keep sending messages without
 	 * a problem even when they are the only active backend.
 	 */
-	min = segP->maxMsgNum;
+	min = pg_atomic_read_u32(&segP->maxMsgNum);
 	minsig = min - SIG_THRESHOLD;
 	lowbound = min - MAXNUMMESSAGES + minFree;
 
@@ -640,8 +626,12 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 */
 	if (min >= MSGNUMWRAPAROUND)
 	{
-		segP->minMsgNum -= MSGNUMWRAPAROUND;
-		segP->maxMsgNum -= MSGNUMWRAPAROUND;
+		/*
+		 * we don't need memory barrier here, but using sub_fetch is just
+		 * simpler than read_u32+write_u32 pair, and this place is not
+		 * contented.
+		 */
+		pg_atomic_sub_fetch_u32(&segP->maxMsgNum, MSGNUMWRAPAROUND);
 		for (i = 0; i < segP->numProcs; i++)
 			segP->procState[segP->pgprocnos[i]].nextMsgNum -= MSGNUMWRAPAROUND;
 	}
@@ -650,7 +640,7 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 * Determine how many messages are still in the queue, and set the
 	 * threshold at which we should repeat SICleanupQueue().
 	 */
-	numMsgs = segP->maxMsgNum - segP->minMsgNum;
+	numMsgs = pg_atomic_read_u32(&segP->maxMsgNum) - segP->minMsgNum;
 	if (numMsgs < CLEANUP_MIN)
 		segP->nextThreshold = CLEANUP_MIN;
 	else
-- 
2.43.0



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

* Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
@ 2025-02-03 16:49 ` Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Heikki Linnakangas @ 2025-02-03 16:49 UTC (permalink / raw)
  To: Yura Sokolov <[email protected]>; [email protected] <[email protected]>

On 03/02/2025 13:05, Yura Sokolov wrote:
> Investigating some performance issues of a client, our engineers found
> msgnumLock to be contended.
> 
> Looking closer it is obvious it is not needed at all: it used only as
> memory barrier. It is even stated in comment at file start:
> 
> * We deal with that by having a spinlock that readers must take for just
> * long enough to read maxMsgNum, while writers take it for just long enough
> * to write maxMsgNum.  (The exact rule is that you need the spinlock to
> * read maxMsgNum if you are not holding SInvalWriteLock, and you need the
> * spinlock to write maxMsgNum unless you are holding both locks.)
> *
> * Note: since maxMsgNum is an int and hence presumably atomically readable/
> * writable, the spinlock might seem unnecessary.  The reason it is needed
> * is to provide a memory barrier: we need to be sure that messages written
> * to the array are actually there before maxMsgNum is increased, and that
> * readers will see that data after fetching maxMsgNum.
> 
> So we changed maxMsgNum to be pg_atomic_uint32, and put appropriate memory
> barriers:
> - pg_write_barrier() before write to maxMsgNum (when only SInvalWriteLock
> is held)
> - pg_read_barrier() after read of maxMsgNum (when only SInvalReadLock is held)
> 
> It improved situation for our client.
> 
> Note: pg_(write|read)_barrier() is chosen instead of
> pg_atomic_(read|write)_membarrier_u32() because it certainly cheaper at
> least on x86_64 where it is translated to just a compiler barrier (empty
> asm statement).
> At least pg_atomic_read_membarrier_u32() is implemented currently as a
> write operation, that's not good for contended place.

Makes sense.


> @@ -640,8 +626,12 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
>  	 */
>  	if (min >= MSGNUMWRAPAROUND)
>  	{
> -		segP->minMsgNum -= MSGNUMWRAPAROUND;
> -		segP->maxMsgNum -= MSGNUMWRAPAROUND;
> +		/*
> +		 * we don't need memory barrier here, but using sub_fetch is just
> +		 * simpler than read_u32+write_u32 pair, and this place is not
> +		 * contented.
> +		 */
> +		pg_atomic_sub_fetch_u32(&segP->maxMsgNum, MSGNUMWRAPAROUND);
>  		for (i = 0; i < segP->numProcs; i++)
>  			segP->procState[segP->pgprocnos[i]].nextMsgNum -= MSGNUMWRAPAROUND;
>  	}

Did you lose the 'segP->minMsgNum -= MSGNUMWRAPAROUND;' here? Do we have 
any tests for the wraparound?

Now that maxMsgNum is unsigned, should we switch to uint32 for minMsgNum 
and nextThreshold for consistency? They still don't need to be atomic 
IIRC, they're protected by SInvalReadLock and SInvalWriteLock.

I kind of wonder why we need that MSGNUMWRAPAROUND limit at all; can't 
we just let the integer wrap around naturally? (after switching to 
unsigned, that is). That doesn't need to be part of this patch though, 
it can be done separately, if it's worthwhile..
.
-- 
Heikki Linnakangas
Neon (https://neon.tech)







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

* Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
@ 2025-02-04 10:36   ` Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-02-04 10:36 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

03.02.2025 19:49, Heikki Linnakangas пишет:
> On 03/02/2025 13:05, Yura Sokolov wrote:
>> Investigating some performance issues of a client, our engineers found
>> msgnumLock to be contended.
>>
>> Looking closer it is obvious it is not needed at all: it used only as
>> memory barrier. It is even stated in comment at file start:
>>
>> * We deal with that by having a spinlock that readers must take for just
>> * long enough to read maxMsgNum, while writers take it for just long enough
>> * to write maxMsgNum.  (The exact rule is that you need the spinlock to
>> * read maxMsgNum if you are not holding SInvalWriteLock, and you need the
>> * spinlock to write maxMsgNum unless you are holding both locks.)
>> *
>> * Note: since maxMsgNum is an int and hence presumably atomically readable/
>> * writable, the spinlock might seem unnecessary.  The reason it is needed
>> * is to provide a memory barrier: we need to be sure that messages written
>> * to the array are actually there before maxMsgNum is increased, and that
>> * readers will see that data after fetching maxMsgNum.
>>
>> So we changed maxMsgNum to be pg_atomic_uint32, and put appropriate memory
>> barriers:
>> - pg_write_barrier() before write to maxMsgNum (when only SInvalWriteLock
>> is held)
>> - pg_read_barrier() after read of maxMsgNum (when only SInvalReadLock is held)
>>
>> It improved situation for our client.
>>
>> Note: pg_(write|read)_barrier() is chosen instead of
>> pg_atomic_(read|write)_membarrier_u32() because it certainly cheaper at
>> least on x86_64 where it is translated to just a compiler barrier (empty
>> asm statement).
>> At least pg_atomic_read_membarrier_u32() is implemented currently as a
>> write operation, that's not good for contended place.
> 
> Makes sense.
> 
> 
>> @@ -640,8 +626,12 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
>>  	 */
>>  	if (min >= MSGNUMWRAPAROUND)
>>  	{
>> -		segP->minMsgNum -= MSGNUMWRAPAROUND;
>> -		segP->maxMsgNum -= MSGNUMWRAPAROUND;
>> +		/*
>> +		 * we don't need memory barrier here, but using sub_fetch is just
>> +		 * simpler than read_u32+write_u32 pair, and this place is not
>> +		 * contented.
>> +		 */
>> +		pg_atomic_sub_fetch_u32(&segP->maxMsgNum, MSGNUMWRAPAROUND);
>>  		for (i = 0; i < segP->numProcs; i++)
>>  			segP->procState[segP->pgprocnos[i]].nextMsgNum -= MSGNUMWRAPAROUND;
>>  	}
> 
> Did you lose the 'segP->minMsgNum -= MSGNUMWRAPAROUND;' here? Do we have 
> any tests for the wraparound?

Oops, yes, thanks. I didn't miss it in private version, but lose during
cherry-picking on top of 'master'. Fixed.

Also removed comment above sub_fetch.

> Now that maxMsgNum is unsigned, should we switch to uint32 for minMsgNum 
> and nextThreshold for consistency? They still don't need to be atomic 
> IIRC, they're protected by SInvalReadLock and SInvalWriteLock.

Ok, I did. And couple of local variables.
There are signed arguments of SIInsertDataEntries, SIGetDataEntries,
SICleanupQueue . Should they be changed to unsigned?

> I kind of wonder why we need that MSGNUMWRAPAROUND limit at all; can't 
> we just let the integer wrap around naturally? (after switching to 
> unsigned, that is). That doesn't need to be part of this patch though, 
> it can be done separately, if it's worthwhile...

There still will be need to handle wraparound, but in comparisons then.
Overall, code will not be simpler, I think. Are there any other benefits
from its removal?

-------

regards,
Yura Sokolov aka funny-falcon

Attachments:

  [text/x-patch] v1-0001-sinvaladt.c-use-atomic-operations-on-maxMsgNum.patch (7.8K, ../../[email protected]/2-v1-0001-sinvaladt.c-use-atomic-operations-on-maxMsgNum.patch)
  download | inline diff:
From 37cfdbe3e09aee16d68ee001ae3e8501327cd24a Mon Sep 17 00:00:00 2001
From: Yura Sokolov <[email protected]>
Date: Mon, 3 Feb 2025 11:58:33 +0300
Subject: [PATCH v1] sinvaladt.c: use atomic operations on maxMsgNum

msgnumLock spinlock could be highly contended.
Comment states it was used as memory barrier.
Lets use atomic ops with memory barriers directly instead.

Note: patch uses pg_read_barrier()/pg_write_barrier() instead of
pg_atomic_read_membarrier_u32()/pg_atomic_write_membarrier_u32() since
no full barrier semantic is required, and explicit read/write barriers
are cheaper at least on x86_64.
---
 src/backend/storage/ipc/sinvaladt.c | 74 ++++++++++++-----------------
 1 file changed, 30 insertions(+), 44 deletions(-)

diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.c
index 2da91738c32..c3b89afce9f 100644
--- a/src/backend/storage/ipc/sinvaladt.c
+++ b/src/backend/storage/ipc/sinvaladt.c
@@ -86,19 +86,10 @@
  * has no need to touch anyone's ProcState, except in the infrequent cases
  * when SICleanupQueue is needed.  The only point of overlap is that
  * the writer wants to change maxMsgNum while readers need to read it.
- * We deal with that by having a spinlock that readers must take for just
- * long enough to read maxMsgNum, while writers take it for just long enough
- * to write maxMsgNum.  (The exact rule is that you need the spinlock to
- * read maxMsgNum if you are not holding SInvalWriteLock, and you need the
- * spinlock to write maxMsgNum unless you are holding both locks.)
- *
- * Note: since maxMsgNum is an int and hence presumably atomically readable/
- * writable, the spinlock might seem unnecessary.  The reason it is needed
- * is to provide a memory barrier: we need to be sure that messages written
- * to the array are actually there before maxMsgNum is increased, and that
- * readers will see that data after fetching maxMsgNum.  Multiprocessors
- * that have weak memory-ordering guarantees can fail without the memory
- * barrier instructions that are included in the spinlock sequences.
+ * We deal with that by using atomic operations with proper memory barriers.
+ * (The exact rule is that you need the read barrier after atomic read
+ * maxMsgNum if you are not holding SInvalWriteLock, and you need the
+ * write barrier before write maxMsgNum unless you are holding both locks.)
  */
 
 
@@ -139,7 +130,7 @@ typedef struct ProcState
 	/* procPid is zero in an inactive ProcState array entry. */
 	pid_t		procPid;		/* PID of backend, for signaling */
 	/* nextMsgNum is meaningless if procPid == 0 or resetState is true. */
-	int			nextMsgNum;		/* next message number to read */
+	uint32		nextMsgNum;		/* next message number to read */
 	bool		resetState;		/* backend needs to reset its state */
 	bool		signaled;		/* backend has been sent catchup signal */
 	bool		hasMessages;	/* backend has unread messages */
@@ -167,11 +158,9 @@ typedef struct SISeg
 	/*
 	 * General state information
 	 */
-	int			minMsgNum;		/* oldest message still needed */
-	int			maxMsgNum;		/* next message number to be assigned */
-	int			nextThreshold;	/* # of messages to call SICleanupQueue */
-
-	slock_t		msgnumLock;		/* spinlock protecting maxMsgNum */
+	uint32		minMsgNum;		/* oldest message still needed */
+	pg_atomic_uint32 maxMsgNum; /* next message number to be assigned */
+	uint32		nextThreshold;	/* # of messages to call SICleanupQueue */
 
 	/*
 	 * Circular buffer holding shared-inval messages
@@ -243,9 +232,8 @@ SharedInvalShmemInit(void)
 
 	/* Clear message counters, save size of procState array, init spinlock */
 	shmInvalBuffer->minMsgNum = 0;
-	shmInvalBuffer->maxMsgNum = 0;
+	pg_atomic_init_u32(&shmInvalBuffer->maxMsgNum, 0);
 	shmInvalBuffer->nextThreshold = CLEANUP_MIN;
-	SpinLockInit(&shmInvalBuffer->msgnumLock);
 
 	/* The buffer[] array is initially all unused, so we need not fill it */
 
@@ -303,7 +291,7 @@ SharedInvalBackendInit(bool sendOnly)
 
 	/* mark myself active, with all extant messages already read */
 	stateP->procPid = MyProcPid;
-	stateP->nextMsgNum = segP->maxMsgNum;
+	stateP->nextMsgNum = pg_atomic_read_u32(&segP->maxMsgNum);
 	stateP->resetState = false;
 	stateP->signaled = false;
 	stateP->hasMessages = false;
@@ -382,8 +370,8 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 	while (n > 0)
 	{
 		int			nthistime = Min(n, WRITE_QUANTUM);
-		int			numMsgs;
-		int			max;
+		uint32		numMsgs;
+		uint32		max;
 		int			i;
 
 		n -= nthistime;
@@ -399,7 +387,7 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		 */
 		for (;;)
 		{
-			numMsgs = segP->maxMsgNum - segP->minMsgNum;
+			numMsgs = pg_atomic_read_u32(&segP->maxMsgNum) - segP->minMsgNum;
 			if (numMsgs + nthistime > MAXNUMMESSAGES ||
 				numMsgs >= segP->nextThreshold)
 				SICleanupQueue(true, nthistime);
@@ -410,17 +398,16 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		/*
 		 * Insert new message(s) into proper slot of circular buffer
 		 */
-		max = segP->maxMsgNum;
+		max = pg_atomic_read_u32(&segP->maxMsgNum);
 		while (nthistime-- > 0)
 		{
 			segP->buffer[max % MAXNUMMESSAGES] = *data++;
 			max++;
 		}
 
-		/* Update current value of maxMsgNum using spinlock */
-		SpinLockAcquire(&segP->msgnumLock);
-		segP->maxMsgNum = max;
-		SpinLockRelease(&segP->msgnumLock);
+		/* Update current value of maxMsgNum using atomic with memory barrier */
+		pg_write_barrier();
+		pg_atomic_write_u32(&segP->maxMsgNum, max);
 
 		/*
 		 * Now that the maxMsgNum change is globally visible, we give everyone
@@ -473,7 +460,7 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 {
 	SISeg	   *segP;
 	ProcState  *stateP;
-	int			max;
+	uint32		max;
 	int			n;
 
 	segP = shmInvalBuffer;
@@ -506,10 +493,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 	 */
 	stateP->hasMessages = false;
 
-	/* Fetch current value of maxMsgNum using spinlock */
-	SpinLockAcquire(&segP->msgnumLock);
-	max = segP->maxMsgNum;
-	SpinLockRelease(&segP->msgnumLock);
+	/* Fetch current value of maxMsgNum using atomic with memory barrier */
+	max = pg_atomic_read_u32(&segP->maxMsgNum);
+	pg_read_barrier();
 
 	if (stateP->resetState)
 	{
@@ -576,11 +562,11 @@ void
 SICleanupQueue(bool callerHasWriteLock, int minFree)
 {
 	SISeg	   *segP = shmInvalBuffer;
-	int			min,
+	uint32		min,
 				minsig,
 				lowbound,
-				numMsgs,
-				i;
+				numMsgs;
+	int			i;
 	ProcState  *needSig = NULL;
 
 	/* Lock out all writers and readers */
@@ -595,14 +581,14 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 * backends here it is possible for them to keep sending messages without
 	 * a problem even when they are the only active backend.
 	 */
-	min = segP->maxMsgNum;
-	minsig = min - SIG_THRESHOLD;
-	lowbound = min - MAXNUMMESSAGES + minFree;
+	min = pg_atomic_read_u32(&segP->maxMsgNum);
+	minsig = min - Min(min, SIG_THRESHOLD);
+	lowbound = min - Min(min, MAXNUMMESSAGES - minFree);
 
 	for (i = 0; i < segP->numProcs; i++)
 	{
 		ProcState  *stateP = &segP->procState[segP->pgprocnos[i]];
-		int			n = stateP->nextMsgNum;
+		uint32		n = stateP->nextMsgNum;
 
 		/* Ignore if already in reset state */
 		Assert(stateP->procPid != 0);
@@ -641,7 +627,7 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	if (min >= MSGNUMWRAPAROUND)
 	{
 		segP->minMsgNum -= MSGNUMWRAPAROUND;
-		segP->maxMsgNum -= MSGNUMWRAPAROUND;
+		pg_atomic_sub_fetch_u32(&segP->maxMsgNum, MSGNUMWRAPAROUND);
 		for (i = 0; i < segP->numProcs; i++)
 			segP->procState[segP->pgprocnos[i]].nextMsgNum -= MSGNUMWRAPAROUND;
 	}
@@ -650,7 +636,7 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 * Determine how many messages are still in the queue, and set the
 	 * threshold at which we should repeat SICleanupQueue().
 	 */
-	numMsgs = segP->maxMsgNum - segP->minMsgNum;
+	numMsgs = pg_atomic_read_u32(&segP->maxMsgNum) - segP->minMsgNum;
 	if (numMsgs < CLEANUP_MIN)
 		segP->nextThreshold = CLEANUP_MIN;
 	else
-- 
2.43.0



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

* Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
@ 2025-03-21 11:35     ` Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-03-21 11:35 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Just rebased the patch.

-------
regards
Yura Sokolov aka funny-falcon

Attachments:

  [text/x-patch] v2-0001-sinvaladt.c-use-atomic-operations-on-maxMsgNum.patch (7.8K, ../../[email protected]/2-v2-0001-sinvaladt.c-use-atomic-operations-on-maxMsgNum.patch)
  download | inline diff:
From 080c9e0de5e6e10751347e1ff50b65df424744cb Mon Sep 17 00:00:00 2001
From: Yura Sokolov <[email protected]>
Date: Mon, 3 Feb 2025 11:58:33 +0300
Subject: [PATCH v2] sinvaladt.c: use atomic operations on maxMsgNum

msgnumLock spinlock could be highly contended.
Comment states it was used as memory barrier.
Lets use atomic ops with memory barriers directly instead.

Note: patch uses pg_read_barrier()/pg_write_barrier() instead of
pg_atomic_read_membarrier_u32()/pg_atomic_write_membarrier_u32() since
no full barrier semantic is required, and explicit read/write barriers
are cheaper at least on x86_64.
---
 src/backend/storage/ipc/sinvaladt.c | 74 ++++++++++++-----------------
 1 file changed, 30 insertions(+), 44 deletions(-)

diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.c
index 2da91738c32..c3b89afce9f 100644
--- a/src/backend/storage/ipc/sinvaladt.c
+++ b/src/backend/storage/ipc/sinvaladt.c
@@ -86,19 +86,10 @@
  * has no need to touch anyone's ProcState, except in the infrequent cases
  * when SICleanupQueue is needed.  The only point of overlap is that
  * the writer wants to change maxMsgNum while readers need to read it.
- * We deal with that by having a spinlock that readers must take for just
- * long enough to read maxMsgNum, while writers take it for just long enough
- * to write maxMsgNum.  (The exact rule is that you need the spinlock to
- * read maxMsgNum if you are not holding SInvalWriteLock, and you need the
- * spinlock to write maxMsgNum unless you are holding both locks.)
- *
- * Note: since maxMsgNum is an int and hence presumably atomically readable/
- * writable, the spinlock might seem unnecessary.  The reason it is needed
- * is to provide a memory barrier: we need to be sure that messages written
- * to the array are actually there before maxMsgNum is increased, and that
- * readers will see that data after fetching maxMsgNum.  Multiprocessors
- * that have weak memory-ordering guarantees can fail without the memory
- * barrier instructions that are included in the spinlock sequences.
+ * We deal with that by using atomic operations with proper memory barriers.
+ * (The exact rule is that you need the read barrier after atomic read
+ * maxMsgNum if you are not holding SInvalWriteLock, and you need the
+ * write barrier before write maxMsgNum unless you are holding both locks.)
  */
 
 
@@ -139,7 +130,7 @@ typedef struct ProcState
 	/* procPid is zero in an inactive ProcState array entry. */
 	pid_t		procPid;		/* PID of backend, for signaling */
 	/* nextMsgNum is meaningless if procPid == 0 or resetState is true. */
-	int			nextMsgNum;		/* next message number to read */
+	uint32		nextMsgNum;		/* next message number to read */
 	bool		resetState;		/* backend needs to reset its state */
 	bool		signaled;		/* backend has been sent catchup signal */
 	bool		hasMessages;	/* backend has unread messages */
@@ -167,11 +158,9 @@ typedef struct SISeg
 	/*
 	 * General state information
 	 */
-	int			minMsgNum;		/* oldest message still needed */
-	int			maxMsgNum;		/* next message number to be assigned */
-	int			nextThreshold;	/* # of messages to call SICleanupQueue */
-
-	slock_t		msgnumLock;		/* spinlock protecting maxMsgNum */
+	uint32		minMsgNum;		/* oldest message still needed */
+	pg_atomic_uint32 maxMsgNum; /* next message number to be assigned */
+	uint32		nextThreshold;	/* # of messages to call SICleanupQueue */
 
 	/*
 	 * Circular buffer holding shared-inval messages
@@ -243,9 +232,8 @@ SharedInvalShmemInit(void)
 
 	/* Clear message counters, save size of procState array, init spinlock */
 	shmInvalBuffer->minMsgNum = 0;
-	shmInvalBuffer->maxMsgNum = 0;
+	pg_atomic_init_u32(&shmInvalBuffer->maxMsgNum, 0);
 	shmInvalBuffer->nextThreshold = CLEANUP_MIN;
-	SpinLockInit(&shmInvalBuffer->msgnumLock);
 
 	/* The buffer[] array is initially all unused, so we need not fill it */
 
@@ -303,7 +291,7 @@ SharedInvalBackendInit(bool sendOnly)
 
 	/* mark myself active, with all extant messages already read */
 	stateP->procPid = MyProcPid;
-	stateP->nextMsgNum = segP->maxMsgNum;
+	stateP->nextMsgNum = pg_atomic_read_u32(&segP->maxMsgNum);
 	stateP->resetState = false;
 	stateP->signaled = false;
 	stateP->hasMessages = false;
@@ -382,8 +370,8 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 	while (n > 0)
 	{
 		int			nthistime = Min(n, WRITE_QUANTUM);
-		int			numMsgs;
-		int			max;
+		uint32		numMsgs;
+		uint32		max;
 		int			i;
 
 		n -= nthistime;
@@ -399,7 +387,7 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		 */
 		for (;;)
 		{
-			numMsgs = segP->maxMsgNum - segP->minMsgNum;
+			numMsgs = pg_atomic_read_u32(&segP->maxMsgNum) - segP->minMsgNum;
 			if (numMsgs + nthistime > MAXNUMMESSAGES ||
 				numMsgs >= segP->nextThreshold)
 				SICleanupQueue(true, nthistime);
@@ -410,17 +398,16 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		/*
 		 * Insert new message(s) into proper slot of circular buffer
 		 */
-		max = segP->maxMsgNum;
+		max = pg_atomic_read_u32(&segP->maxMsgNum);
 		while (nthistime-- > 0)
 		{
 			segP->buffer[max % MAXNUMMESSAGES] = *data++;
 			max++;
 		}
 
-		/* Update current value of maxMsgNum using spinlock */
-		SpinLockAcquire(&segP->msgnumLock);
-		segP->maxMsgNum = max;
-		SpinLockRelease(&segP->msgnumLock);
+		/* Update current value of maxMsgNum using atomic with memory barrier */
+		pg_write_barrier();
+		pg_atomic_write_u32(&segP->maxMsgNum, max);
 
 		/*
 		 * Now that the maxMsgNum change is globally visible, we give everyone
@@ -473,7 +460,7 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 {
 	SISeg	   *segP;
 	ProcState  *stateP;
-	int			max;
+	uint32		max;
 	int			n;
 
 	segP = shmInvalBuffer;
@@ -506,10 +493,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 	 */
 	stateP->hasMessages = false;
 
-	/* Fetch current value of maxMsgNum using spinlock */
-	SpinLockAcquire(&segP->msgnumLock);
-	max = segP->maxMsgNum;
-	SpinLockRelease(&segP->msgnumLock);
+	/* Fetch current value of maxMsgNum using atomic with memory barrier */
+	max = pg_atomic_read_u32(&segP->maxMsgNum);
+	pg_read_barrier();
 
 	if (stateP->resetState)
 	{
@@ -576,11 +562,11 @@ void
 SICleanupQueue(bool callerHasWriteLock, int minFree)
 {
 	SISeg	   *segP = shmInvalBuffer;
-	int			min,
+	uint32		min,
 				minsig,
 				lowbound,
-				numMsgs,
-				i;
+				numMsgs;
+	int			i;
 	ProcState  *needSig = NULL;
 
 	/* Lock out all writers and readers */
@@ -595,14 +581,14 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 * backends here it is possible for them to keep sending messages without
 	 * a problem even when they are the only active backend.
 	 */
-	min = segP->maxMsgNum;
-	minsig = min - SIG_THRESHOLD;
-	lowbound = min - MAXNUMMESSAGES + minFree;
+	min = pg_atomic_read_u32(&segP->maxMsgNum);
+	minsig = min - Min(min, SIG_THRESHOLD);
+	lowbound = min - Min(min, MAXNUMMESSAGES - minFree);
 
 	for (i = 0; i < segP->numProcs; i++)
 	{
 		ProcState  *stateP = &segP->procState[segP->pgprocnos[i]];
-		int			n = stateP->nextMsgNum;
+		uint32		n = stateP->nextMsgNum;
 
 		/* Ignore if already in reset state */
 		Assert(stateP->procPid != 0);
@@ -641,7 +627,7 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	if (min >= MSGNUMWRAPAROUND)
 	{
 		segP->minMsgNum -= MSGNUMWRAPAROUND;
-		segP->maxMsgNum -= MSGNUMWRAPAROUND;
+		pg_atomic_sub_fetch_u32(&segP->maxMsgNum, MSGNUMWRAPAROUND);
 		for (i = 0; i < segP->numProcs; i++)
 			segP->procState[segP->pgprocnos[i]].nextMsgNum -= MSGNUMWRAPAROUND;
 	}
@@ -650,7 +636,7 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 * Determine how many messages are still in the queue, and set the
 	 * threshold at which we should repeat SICleanupQueue().
 	 */
-	numMsgs = segP->maxMsgNum - segP->minMsgNum;
+	numMsgs = pg_atomic_read_u32(&segP->maxMsgNum) - segP->minMsgNum;
 	if (numMsgs < CLEANUP_MIN)
 		segP->nextThreshold = CLEANUP_MIN;
 	else
-- 
2.43.0



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

* Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
@ 2025-03-21 16:33       ` Andres Freund <[email protected]>
  2025-03-24 10:41         ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  0 siblings, 2 replies; 84+ messages in thread

From: Andres Freund @ 2025-03-21 16:33 UTC (permalink / raw)
  To: Yura Sokolov <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Hi,

On 2025-03-21 14:35:16 +0300, Yura Sokolov wrote:
> From 080c9e0de5e6e10751347e1ff50b65df424744cb Mon Sep 17 00:00:00 2001
> From: Yura Sokolov <[email protected]>
> Date: Mon, 3 Feb 2025 11:58:33 +0300
> Subject: [PATCH v2] sinvaladt.c: use atomic operations on maxMsgNum
>
> msgnumLock spinlock could be highly contended.
> Comment states it was used as memory barrier.
> Lets use atomic ops with memory barriers directly instead.
>
> Note: patch uses pg_read_barrier()/pg_write_barrier() instead of
> pg_atomic_read_membarrier_u32()/pg_atomic_write_membarrier_u32() since
> no full barrier semantic is required, and explicit read/write barriers
> are cheaper at least on x86_64.

Is it actually true that full barriers aren't required?  I think we might
actually rely on a stronger ordering.


> @@ -506,10 +493,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
>  	 */
>  	stateP->hasMessages = false;
>
> -	/* Fetch current value of maxMsgNum using spinlock */
> -	SpinLockAcquire(&segP->msgnumLock);
> -	max = segP->maxMsgNum;
> -	SpinLockRelease(&segP->msgnumLock);
> +	/* Fetch current value of maxMsgNum using atomic with memory barrier */
> +	max = pg_atomic_read_u32(&segP->maxMsgNum);
> +	pg_read_barrier();
>
>  	if (stateP->resetState)
>  	{
>		/*
>		 * Force reset.  We can say we have dealt with any messages added
>		 * since the reset, as well; and that means we should clear the
>		 * signaled flag, too.
>		 */
>		stateP->nextMsgNum = max;
>		stateP->resetState = false;
>		stateP->signaled = false;
>		LWLockRelease(SInvalReadLock);
>		return -1;
>	}

vs

> @@ -410,17 +398,16 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
>  		/*
>  		 * Insert new message(s) into proper slot of circular buffer
>  		 */
> -		max = segP->maxMsgNum;
> +		max = pg_atomic_read_u32(&segP->maxMsgNum);
>  		while (nthistime-- > 0)
>  		{
>  			segP->buffer[max % MAXNUMMESSAGES] = *data++;
>  			max++;
>  		}
>
> -		/* Update current value of maxMsgNum using spinlock */
> -		SpinLockAcquire(&segP->msgnumLock);
> -		segP->maxMsgNum = max;
> -		SpinLockRelease(&segP->msgnumLock);
> +		/* Update current value of maxMsgNum using atomic with memory barrier */
> +		pg_write_barrier();
> +		pg_atomic_write_u32(&segP->maxMsgNum, max);
>
>  		/*
>  		 * Now that the maxMsgNum change is globally visible, we give everyone
>		 * a swift kick to make sure they read the newly added messages.
>		 * Releasing SInvalWriteLock will enforce a full memory barrier, so
>		 * these (unlocked) changes will be committed to memory before we exit
>		 * the function.
>		 */
>		for (i = 0; i < segP->numProcs; i++)
>		{
>			ProcState  *stateP = &segP->procState[segP->pgprocnos[i]];
>
>			stateP->hasMessages = true;
>		}

On a loosely ordered architecture, the hasMessage=false in SIGetDataEntries()
could be reordered with the read of maxMsgNum. Which, afaict, would lead to
missing messages. That's not prevented by the pg_write_barrier() in
SIInsertDataEntries().  I think there may be other similar dangers.

This could be solved by adding full memory barriers in a few places. But:

I'd also like to know a bit more about the motivation here - I can easily
believe that you hit contention around the shared inval queue, but I find it
somewhat hard to believe that a spinlock that's acquired *once* per batch
("quantum"), covering a single read/write, is going to be the bottleneck,
rather than the much longer held LWLock, that protects iterating over all
procs.

Have you verified that this actually addresses the performance issue?

Greetings,

Andres Freund





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

* Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
@ 2025-03-24 10:41         ` Yura Sokolov <[email protected]>
  2025-03-24 13:08           ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  1 sibling, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-03-24 10:41 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Hi, Andres

21.03.2025 19:33, Andres Freund wrote:
> Hi,
> 
> On 2025-03-21 14:35:16 +0300, Yura Sokolov wrote:
>> From 080c9e0de5e6e10751347e1ff50b65df424744cb Mon Sep 17 00:00:00 2001
>> From: Yura Sokolov <[email protected]>
>> Date: Mon, 3 Feb 2025 11:58:33 +0300
>> Subject: [PATCH v2] sinvaladt.c: use atomic operations on maxMsgNum
>>
>> msgnumLock spinlock could be highly contended.
>> Comment states it was used as memory barrier.
>> Lets use atomic ops with memory barriers directly instead.
>>
>> Note: patch uses pg_read_barrier()/pg_write_barrier() instead of
>> pg_atomic_read_membarrier_u32()/pg_atomic_write_membarrier_u32() since
>> no full barrier semantic is required, and explicit read/write barriers
>> are cheaper at least on x86_64.
> 
> Is it actually true that full barriers aren't required?  I think we might
> actually rely on a stronger ordering.
> 
> 
>> @@ -506,10 +493,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
>>  	 */
>>  	stateP->hasMessages = false;
>>
>> -	/* Fetch current value of maxMsgNum using spinlock */
>> -	SpinLockAcquire(&segP->msgnumLock);
>> -	max = segP->maxMsgNum;
>> -	SpinLockRelease(&segP->msgnumLock);
>> +	/* Fetch current value of maxMsgNum using atomic with memory barrier */
>> +	max = pg_atomic_read_u32(&segP->maxMsgNum);
>> +	pg_read_barrier();
>>
>>  	if (stateP->resetState)
>>  	{
>> 		/*
>> 		 * Force reset.  We can say we have dealt with any messages added
>> 		 * since the reset, as well; and that means we should clear the
>> 		 * signaled flag, too.
>> 		 */
>> 		stateP->nextMsgNum = max;
>> 		stateP->resetState = false;
>> 		stateP->signaled = false;
>> 		LWLockRelease(SInvalReadLock);
>> 		return -1;
>> 	}
> 
> vs
> 
>> @@ -410,17 +398,16 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
>>  		/*
>>  		 * Insert new message(s) into proper slot of circular buffer
>>  		 */
>> -		max = segP->maxMsgNum;
>> +		max = pg_atomic_read_u32(&segP->maxMsgNum);
>>  		while (nthistime-- > 0)
>>  		{
>>  			segP->buffer[max % MAXNUMMESSAGES] = *data++;
>>  			max++;
>>  		}
>>
>> -		/* Update current value of maxMsgNum using spinlock */
>> -		SpinLockAcquire(&segP->msgnumLock);
>> -		segP->maxMsgNum = max;
>> -		SpinLockRelease(&segP->msgnumLock);
>> +		/* Update current value of maxMsgNum using atomic with memory barrier */
>> +		pg_write_barrier();
>> +		pg_atomic_write_u32(&segP->maxMsgNum, max);
>>
>>  		/*
>>  		 * Now that the maxMsgNum change is globally visible, we give everyone
>> 		 * a swift kick to make sure they read the newly added messages.
>> 		 * Releasing SInvalWriteLock will enforce a full memory barrier, so
>> 		 * these (unlocked) changes will be committed to memory before we exit
>> 		 * the function.
>> 		 */
>> 		for (i = 0; i < segP->numProcs; i++)
>> 		{
>> 			ProcState  *stateP = &segP->procState[segP->pgprocnos[i]];
>>
>> 			stateP->hasMessages = true;
>> 		}
> 
> On a loosely ordered architecture, the hasMessage=false in SIGetDataEntries()
> could be reordered with the read of maxMsgNum. Which, afaict, would lead to
> missing messages. That's not prevented by the pg_write_barrier() in
> SIInsertDataEntries().  I think there may be other similar dangers.
> 
> This could be solved by adding full memory barriers in a few places.

Big thanks for review and suggestion!

I agree, pg_memory_barrier should be added before read of segP->maxMsgNum.
I think, change of stateP->hasMessages to atomic variable is better way,
but it will change sizeof ProcState.

I don't see the need to full barrier after read of maxMsgNum, since other
ProcState fields are protected by SInvalReadLock, aren't they? So I leave
read_barrier there.

I still avoid use of read_membarrier since it is actually write operation.
Although pg_memory_barrier is implemented as write operation as well at
x86_64, but on memory cell on process's stack, so it will not be contended.

And atomic_write_membarrier is used to write maxMsgNum just to simplify
code. If backport is considered, then write_barriers before and after could
be used instead.

Fixes version is attached.

> But:
> 
> I'd also like to know a bit more about the motivation here - I can easily
> believe that you hit contention around the shared inval queue, but I find it
> somewhat hard to believe that a spinlock that's acquired *once* per batch
> ("quantum"), covering a single read/write, is going to be the bottleneck,
> rather than the much longer held LWLock, that protects iterating over all
> procs.
> 
> Have you verified that this actually addresses the performance issue?

Problem on this spinlock were observed at least by two independent technical
supports. First, some friendly vendor company shared the idea to remove it.
We don't know exactly their situation. But I suppose it was quite similar
to situation out tech support investigated at our client some months later:

(Cite from tech support report:)
> Almost 20% of CPU time is spend at backtraces like:
    4b0d2d s_lock (/opt/pgpro/ent-15/bin/postgres)
    49c847 SIGetDataEntries
    49bf94 ReceiveSharedInvalidMessages
    4a14ba LockRelationOid
    1671f4 relation_open
    1de1cd table_open
    5e82aa RelationGetStatExtList
    402a01 get_relation_statistics (inlined)
    402a01 get_relation_info
    407a9e build_simple_rel
    3daa1d add_base_rels_to_query
    3daa1d add_base_rels_to_query
    3dd92b query_planner


Client has many NUMA-nodes in single machine, and software actively
generates invalidation messages (probably, due active usage of temporary
tables).

Since, backtrace is quite obvious and ends at s_lock, the patch have to help.

-- 
regards
Yura Sokolov aka funny-falcon

Attachments:

  [text/x-patch] v3-0001-sinvaladt.c-use-atomic-operations-on-maxMsgNum.patch (7.6K, ../../[email protected]/2-v3-0001-sinvaladt.c-use-atomic-operations-on-maxMsgNum.patch)
  download | inline diff:
From de96fd271a107ce4300f758822d7cc2f9ab1ff1c Mon Sep 17 00:00:00 2001
From: Yura Sokolov <[email protected]>
Date: Mon, 3 Feb 2025 11:58:33 +0300
Subject: [PATCH v3] sinvaladt.c: use atomic operations on maxMsgNum

msgnumLock spinlock could be contended.
Comment states it was used as memory barrier.
Lets use atomic ops with memory barriers directly instead.
---
 src/backend/storage/ipc/sinvaladt.c | 80 +++++++++++++----------------
 1 file changed, 36 insertions(+), 44 deletions(-)

diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.c
index 2da91738c32..400bfdb0072 100644
--- a/src/backend/storage/ipc/sinvaladt.c
+++ b/src/backend/storage/ipc/sinvaladt.c
@@ -86,19 +86,7 @@
  * has no need to touch anyone's ProcState, except in the infrequent cases
  * when SICleanupQueue is needed.  The only point of overlap is that
  * the writer wants to change maxMsgNum while readers need to read it.
- * We deal with that by having a spinlock that readers must take for just
- * long enough to read maxMsgNum, while writers take it for just long enough
- * to write maxMsgNum.  (The exact rule is that you need the spinlock to
- * read maxMsgNum if you are not holding SInvalWriteLock, and you need the
- * spinlock to write maxMsgNum unless you are holding both locks.)
- *
- * Note: since maxMsgNum is an int and hence presumably atomically readable/
- * writable, the spinlock might seem unnecessary.  The reason it is needed
- * is to provide a memory barrier: we need to be sure that messages written
- * to the array are actually there before maxMsgNum is increased, and that
- * readers will see that data after fetching maxMsgNum.  Multiprocessors
- * that have weak memory-ordering guarantees can fail without the memory
- * barrier instructions that are included in the spinlock sequences.
+ * We deal with that by using atomic operations with proper memory barriers.
  */
 
 
@@ -139,7 +127,7 @@ typedef struct ProcState
 	/* procPid is zero in an inactive ProcState array entry. */
 	pid_t		procPid;		/* PID of backend, for signaling */
 	/* nextMsgNum is meaningless if procPid == 0 or resetState is true. */
-	int			nextMsgNum;		/* next message number to read */
+	uint32		nextMsgNum;		/* next message number to read */
 	bool		resetState;		/* backend needs to reset its state */
 	bool		signaled;		/* backend has been sent catchup signal */
 	bool		hasMessages;	/* backend has unread messages */
@@ -167,11 +155,9 @@ typedef struct SISeg
 	/*
 	 * General state information
 	 */
-	int			minMsgNum;		/* oldest message still needed */
-	int			maxMsgNum;		/* next message number to be assigned */
-	int			nextThreshold;	/* # of messages to call SICleanupQueue */
-
-	slock_t		msgnumLock;		/* spinlock protecting maxMsgNum */
+	uint32		minMsgNum;		/* oldest message still needed */
+	pg_atomic_uint32 maxMsgNum; /* next message number to be assigned */
+	uint32		nextThreshold;	/* # of messages to call SICleanupQueue */
 
 	/*
 	 * Circular buffer holding shared-inval messages
@@ -243,9 +229,8 @@ SharedInvalShmemInit(void)
 
 	/* Clear message counters, save size of procState array, init spinlock */
 	shmInvalBuffer->minMsgNum = 0;
-	shmInvalBuffer->maxMsgNum = 0;
+	pg_atomic_init_u32(&shmInvalBuffer->maxMsgNum, 0);
 	shmInvalBuffer->nextThreshold = CLEANUP_MIN;
-	SpinLockInit(&shmInvalBuffer->msgnumLock);
 
 	/* The buffer[] array is initially all unused, so we need not fill it */
 
@@ -303,7 +288,7 @@ SharedInvalBackendInit(bool sendOnly)
 
 	/* mark myself active, with all extant messages already read */
 	stateP->procPid = MyProcPid;
-	stateP->nextMsgNum = segP->maxMsgNum;
+	stateP->nextMsgNum = pg_atomic_read_u32(&segP->maxMsgNum);
 	stateP->resetState = false;
 	stateP->signaled = false;
 	stateP->hasMessages = false;
@@ -382,8 +367,8 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 	while (n > 0)
 	{
 		int			nthistime = Min(n, WRITE_QUANTUM);
-		int			numMsgs;
-		int			max;
+		uint32		numMsgs;
+		uint32		max;
 		int			i;
 
 		n -= nthistime;
@@ -399,7 +384,7 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		 */
 		for (;;)
 		{
-			numMsgs = segP->maxMsgNum - segP->minMsgNum;
+			numMsgs = pg_atomic_read_u32(&segP->maxMsgNum) - segP->minMsgNum;
 			if (numMsgs + nthistime > MAXNUMMESSAGES ||
 				numMsgs >= segP->nextThreshold)
 				SICleanupQueue(true, nthistime);
@@ -410,17 +395,20 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		/*
 		 * Insert new message(s) into proper slot of circular buffer
 		 */
-		max = segP->maxMsgNum;
+		max = pg_atomic_read_u32(&segP->maxMsgNum);
 		while (nthistime-- > 0)
 		{
 			segP->buffer[max % MAXNUMMESSAGES] = *data++;
 			max++;
 		}
 
-		/* Update current value of maxMsgNum using spinlock */
-		SpinLockAcquire(&segP->msgnumLock);
-		segP->maxMsgNum = max;
-		SpinLockRelease(&segP->msgnumLock);
+		/*
+		 * We need to write maxMsgNum strictly after write of messages and
+		 * strictly before write to hasMessages. Two write barriers could be
+		 * used before and after. But to simplify code, write_membarrier is
+		 * used.
+		 */
+		pg_atomic_write_membarrier_u32(&segP->maxMsgNum, max);
 
 		/*
 		 * Now that the maxMsgNum change is globally visible, we give everyone
@@ -473,7 +461,7 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 {
 	SISeg	   *segP;
 	ProcState  *stateP;
-	int			max;
+	uint32		max;
 	int			n;
 
 	segP = shmInvalBuffer;
@@ -506,10 +494,14 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 	 */
 	stateP->hasMessages = false;
 
-	/* Fetch current value of maxMsgNum using spinlock */
-	SpinLockAcquire(&segP->msgnumLock);
-	max = segP->maxMsgNum;
-	SpinLockRelease(&segP->msgnumLock);
+	/*
+	 * Full barrier before read of maxMsgNum is to synchronize against
+	 * hasMessages=false. To synchronize message reads read barrier after is
+	 * enough.
+	 */
+	pg_memory_barrier();
+	max = pg_atomic_read_u32(&segP->maxMsgNum);
+	pg_read_barrier();
 
 	if (stateP->resetState)
 	{
@@ -576,11 +568,11 @@ void
 SICleanupQueue(bool callerHasWriteLock, int minFree)
 {
 	SISeg	   *segP = shmInvalBuffer;
-	int			min,
+	uint32		min,
 				minsig,
 				lowbound,
-				numMsgs,
-				i;
+				numMsgs;
+	int			i;
 	ProcState  *needSig = NULL;
 
 	/* Lock out all writers and readers */
@@ -595,14 +587,14 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 * backends here it is possible for them to keep sending messages without
 	 * a problem even when they are the only active backend.
 	 */
-	min = segP->maxMsgNum;
-	minsig = min - SIG_THRESHOLD;
-	lowbound = min - MAXNUMMESSAGES + minFree;
+	min = pg_atomic_read_u32(&segP->maxMsgNum);
+	minsig = min - Min(min, SIG_THRESHOLD);
+	lowbound = min - Min(min, MAXNUMMESSAGES - minFree);
 
 	for (i = 0; i < segP->numProcs; i++)
 	{
 		ProcState  *stateP = &segP->procState[segP->pgprocnos[i]];
-		int			n = stateP->nextMsgNum;
+		uint32		n = stateP->nextMsgNum;
 
 		/* Ignore if already in reset state */
 		Assert(stateP->procPid != 0);
@@ -641,7 +633,7 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	if (min >= MSGNUMWRAPAROUND)
 	{
 		segP->minMsgNum -= MSGNUMWRAPAROUND;
-		segP->maxMsgNum -= MSGNUMWRAPAROUND;
+		pg_atomic_sub_fetch_u32(&segP->maxMsgNum, MSGNUMWRAPAROUND);
 		for (i = 0; i < segP->numProcs; i++)
 			segP->procState[segP->pgprocnos[i]].nextMsgNum -= MSGNUMWRAPAROUND;
 	}
@@ -650,7 +642,7 @@ SICleanupQueue(bool callerHasWriteLock, int minFree)
 	 * Determine how many messages are still in the queue, and set the
 	 * threshold at which we should repeat SICleanupQueue().
 	 */
-	numMsgs = segP->maxMsgNum - segP->minMsgNum;
+	numMsgs = pg_atomic_read_u32(&segP->maxMsgNum) - segP->minMsgNum;
 	if (numMsgs < CLEANUP_MIN)
 		segP->nextThreshold = CLEANUP_MIN;
 	else
-- 
2.43.0



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

* Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-03-24 10:41         ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
@ 2025-03-24 13:08           ` Andres Freund <[email protected]>
  2025-03-25 10:52             ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Andres Freund @ 2025-03-24 13:08 UTC (permalink / raw)
  To: Yura Sokolov <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Hi,

On 2025-03-24 13:41:17 +0300, Yura Sokolov wrote:
> 21.03.2025 19:33, Andres Freund wrote:
> > I'd also like to know a bit more about the motivation here - I can easily
> > believe that you hit contention around the shared inval queue, but I find it
> > somewhat hard to believe that a spinlock that's acquired *once* per batch
> > ("quantum"), covering a single read/write, is going to be the bottleneck,
> > rather than the much longer held LWLock, that protects iterating over all
> > procs.
> > 
> > Have you verified that this actually addresses the performance issue?
> 
> Problem on this spinlock were observed at least by two independent technical
> supports. First, some friendly vendor company shared the idea to remove it.
> We don't know exactly their situation. But I suppose it was quite similar
> to situation out tech support investigated at our client some months later:
> 
> (Cite from tech support report:)
> > Almost 20% of CPU time is spend at backtraces like:
>     4b0d2d s_lock (/opt/pgpro/ent-15/bin/postgres)
>     49c847 SIGetDataEntries
>     49bf94 ReceiveSharedInvalidMessages
>     4a14ba LockRelationOid
>     1671f4 relation_open
>     1de1cd table_open
>     5e82aa RelationGetStatExtList
>     402a01 get_relation_statistics (inlined)
>     402a01 get_relation_info
>     407a9e build_simple_rel
>     3daa1d add_base_rels_to_query
>     3daa1d add_base_rels_to_query
>     3dd92b query_planner
> 
> 
> Client has many NUMA-nodes in single machine, and software actively
> generates invalidation messages (probably, due active usage of temporary
> tables).
> 
> Since, backtrace is quite obvious and ends at s_lock, the patch have to help.

I don't believe we have the whole story here. It just doesn't seem plausible
that, with the current code, the spinlock in SIGetDataEntries() would be the
bottleneck, rather than contention on the lwlock. The spinlock just covers a
*single read*.  Unless pgpro has modified the relevant code?

One possible explanation for why the spinlock shows up so badly is that it is
due to false sharing. Note that SiSeg->msgnumLock and the start of
SiSeg->buffer[] are on the same cache line.

How was this "Almost 20% of CPU time is spend at backtraces like" determined?

Greetings,

Andres Freund





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

* Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-03-24 10:41         ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-24 13:08           ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
@ 2025-03-25 10:52             ` Yura Sokolov <[email protected]>
  2025-03-25 11:45               ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-03-25 10:52 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Good day, Andres

24.03.2025 16:08, Andres Freund wrote:
> On 2025-03-24 13:41:17 +0300, Yura Sokolov wrote:
>> 21.03.2025 19:33, Andres Freund wrote:
>>> I'd also like to know a bit more about the motivation here - I can easily
>>> believe that you hit contention around the shared inval queue, but I
find it
>>> somewhat hard to believe that a spinlock that's acquired *once* per batch
>>> ("quantum"), covering a single read/write, is going to be the bottleneck,
>>> rather than the much longer held LWLock, that protects iterating over all
>>> procs.
>>>
>>> Have you verified that this actually addresses the performance issue?
>>
>> Problem on this spinlock were observed at least by two independent technical
>> supports. First, some friendly vendor company shared the idea to remove it.
>> We don't know exactly their situation. But I suppose it was quite similar
>> to situation out tech support investigated at our client some months later:
>>
>> (Cite from tech support report:)
>>> Almost 20% of CPU time is spend at backtraces like:
>>     4b0d2d s_lock (/opt/pgpro/ent-15/bin/postgres)
>>     49c847 SIGetDataEntries
>>     49bf94 ReceiveSharedInvalidMessages
>>     4a14ba LockRelationOid
>>     1671f4 relation_open
>>     1de1cd table_open
>>     5e82aa RelationGetStatExtList
>>     402a01 get_relation_statistics (inlined)
>>     402a01 get_relation_info
>>     407a9e build_simple_rel
>>     3daa1d add_base_rels_to_query
>>     3daa1d add_base_rels_to_query
>>     3dd92b query_planner
>>
>>
>> Client has many NUMA-nodes in single machine, and software actively
>> generates invalidation messages (probably, due active usage of temporary
>> tables).
>>
>> Since, backtrace is quite obvious and ends at s_lock, the patch have to
help.
>
> I don't believe we have the whole story here. It just doesn't seem plausible
> that, with the current code, the spinlock in SIGetDataEntries() would be the
> bottleneck, rather than contention on the lwlock. The spinlock just covers a
> *single read*.  Unless pgpro has modified the relevant code?
>
> One possible explanation for why the spinlock shows up so badly is that it is
> due to false sharing. Note that SiSeg->msgnumLock and the start of
> SiSeg->buffer[] are on the same cache line.
>
> How was this "Almost 20% of CPU time is spend at backtraces like" determined?

Excuse me I didn't attached flamegraph collected by our tech support from
client's server during peak of the problem. So I attach it now.

If you open it in browser and search for "SIGetDataEntries", you'll see it
consumes 18.4%. It is not single large bar. Instead there are dozens of
calls to SIGetDataEntries, and every one spend almost all its time in
s_lock. If you search for s_lock, it consumes 16.9%, and almost every call
to s_lock is from SIGetDataEntries.

Looks like, we call to ReceiveSharedInvalidMessages
(AcceptInvalidationMessages, actually) too frequently during planing. And
if there are large stream of invalidation messages, SIGetDataEntries have
some work very frequently. Therefore many backends, which plans their
queries at the moment, start to fight for msgNumLock.

If ReceiveSharedInvalidMessages (and SIGetDataEntries by it) were called
rarely, then you conclusion were right: taking spinlock around just read of
one variable before processing large batch of messages looks to not be
source of problem. But since it is called very frequently, and stream of
messages is high, "there is always few new messages".

As I've said, it is most probably due to use of famous 1C software, which
uses a lot of temporary tables. So it generates high amount of invalidation
messages. We've patched pgpro postgres to NOT SEND most of invalidation
messages generated by temporary tables, but it is difficult to not send all
of such.


-- 
regards
Yura Sokolov aka funny-falcon

Attachments:

  [application/gzip] perf20250123_1120.lst.bind.svg.gz (343.4K, ../../[email protected]/2-perf20250123_1120.lst.bind.svg.gz)
  download

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

* Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-03-24 10:41         ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-24 13:08           ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-03-25 10:52             ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
@ 2025-03-25 11:45               ` Yura Sokolov <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Yura Sokolov @ 2025-03-25 11:45 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

25.03.2025 13:52, Yura Sokolov пишет:
> Good day, Andres
> 
> 24.03.2025 16:08, Andres Freund wrote:
>> On 2025-03-24 13:41:17 +0300, Yura Sokolov wrote:
>>> 21.03.2025 19:33, Andres Freund wrote:
>>>> I'd also like to know a bit more about the motivation here - I can easily
>>>> believe that you hit contention around the shared inval queue, but I
> find it
>>>> somewhat hard to believe that a spinlock that's acquired *once* per batch
>>>> ("quantum"), covering a single read/write, is going to be the bottleneck,
>>>> rather than the much longer held LWLock, that protects iterating over all
>>>> procs.
>>>>
>>>> Have you verified that this actually addresses the performance issue?
>>>
>>> Problem on this spinlock were observed at least by two independent technical
>>> supports. First, some friendly vendor company shared the idea to remove it.
>>> We don't know exactly their situation. But I suppose it was quite similar
>>> to situation out tech support investigated at our client some months later:
>>>
>>> (Cite from tech support report:)
>>>> Almost 20% of CPU time is spend at backtraces like:
>>>     4b0d2d s_lock (/opt/pgpro/ent-15/bin/postgres)
>>>     49c847 SIGetDataEntries
>>>     49bf94 ReceiveSharedInvalidMessages
>>>     4a14ba LockRelationOid
>>>     1671f4 relation_open
>>>     1de1cd table_open
>>>     5e82aa RelationGetStatExtList
>>>     402a01 get_relation_statistics (inlined)
>>>     402a01 get_relation_info
>>>     407a9e build_simple_rel
>>>     3daa1d add_base_rels_to_query
>>>     3daa1d add_base_rels_to_query
>>>     3dd92b query_planner
>>>
>>>
>>> Client has many NUMA-nodes in single machine, and software actively
>>> generates invalidation messages (probably, due active usage of temporary
>>> tables).
>>>
>>> Since, backtrace is quite obvious and ends at s_lock, the patch have to
> help.
>>
>> I don't believe we have the whole story here. It just doesn't seem plausible
>> that, with the current code, the spinlock in SIGetDataEntries() would be the
>> bottleneck, rather than contention on the lwlock. The spinlock just covers a
>> *single read*.  Unless pgpro has modified the relevant code?
>>
>> One possible explanation for why the spinlock shows up so badly is that it is
>> due to false sharing. Note that SiSeg->msgnumLock and the start of
>> SiSeg->buffer[] are on the same cache line.
>>
>> How was this "Almost 20% of CPU time is spend at backtraces like" determined?
> 
> Excuse me I didn't attached flamegraph collected by our tech support from
> client's server during peak of the problem. So I attach it now.
> 
> If you open it in browser and search for "SIGetDataEntries", you'll see it
> consumes 18.4%. It is not single large bar. Instead there are dozens of
> calls to SIGetDataEntries, and every one spend almost all its time in
> s_lock. If you search for s_lock, it consumes 16.9%, and almost every call
> to s_lock is from SIGetDataEntries.
> 
> Looks like, we call to ReceiveSharedInvalidMessages
> (AcceptInvalidationMessages, actually) too frequently during planing. And
> if there are large stream of invalidation messages, SIGetDataEntries have
> some work very frequently. Therefore many backends, which plans their
> queries at the moment, start to fight for msgNumLock.
>
> If ReceiveSharedInvalidMessages (and SIGetDataEntries by it) were called
> rarely, then you conclusion were right: taking spinlock around just read of
> one variable before processing large batch of messages looks to not be
> source of problem. But since it is called very frequently, and stream of
> messages is high, "there is always few new messages".

And one more reason for contention: ReceiveSharedInvalidMessages calls
SIGetDataEntries for batches sized MAXINVALMSGS (=32). It calls it again
and again until queue is empty. There could be a lot of messages pushed to
a queue.

In fact, we have to increase MAXNUMMESSAGES up to 16k to not fall into
InvalidateSystemCaches (which costs much more), so theoretically
SIGetDataEntries could be called upto 512 times per
ReceiveSharedInvalidMessages.

And it is not so theoretically, since COMMIT of transaction may send
thousands of invalidation messages at once.

Probably, increasing MAXINVALMSGS is a good idea as well.

-- 
regards
Yura Sokolov aka funny-falcon





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

* SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
@ 2025-05-05 07:30         ` Yura Sokolov <[email protected]>
  2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  1 sibling, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-05-05 07:30 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

21.03.2025 19:33, Andres Freund пишет:
> Hi,
> 
> On 2025-03-21 14:35:16 +0300, Yura Sokolov wrote:
>> From 080c9e0de5e6e10751347e1ff50b65df424744cb Mon Sep 17 00:00:00 2001
>> From: Yura Sokolov <[email protected]>
>> Date: Mon, 3 Feb 2025 11:58:33 +0300
>> Subject: [PATCH v2] sinvaladt.c: use atomic operations on maxMsgNum
>>
>> msgnumLock spinlock could be highly contended.
>> Comment states it was used as memory barrier.
>> Lets use atomic ops with memory barriers directly instead.
>>
>> Note: patch uses pg_read_barrier()/pg_write_barrier() instead of
>> pg_atomic_read_membarrier_u32()/pg_atomic_write_membarrier_u32() since
>> no full barrier semantic is required, and explicit read/write barriers
>> are cheaper at least on x86_64.
> 
> Is it actually true that full barriers aren't required?  I think we might
> actually rely on a stronger ordering.
> 
> 
>> @@ -506,10 +493,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
>>  	 */
>>  	stateP->hasMessages = false;
>>
>> -	/* Fetch current value of maxMsgNum using spinlock */
>> -	SpinLockAcquire(&segP->msgnumLock);
>> -	max = segP->maxMsgNum;
>> -	SpinLockRelease(&segP->msgnumLock);
>> +	/* Fetch current value of maxMsgNum using atomic with memory barrier */
>> +	max = pg_atomic_read_u32(&segP->maxMsgNum);
>> +	pg_read_barrier();
>>
>>  	if (stateP->resetState)
>>  	{
>> 		/*
>> 		 * Force reset.  We can say we have dealt with any messages added
>> 		 * since the reset, as well; and that means we should clear the
>> 		 * signaled flag, too.
>> 		 */
>> 		stateP->nextMsgNum = max;
>> 		stateP->resetState = false;
>> 		stateP->signaled = false;
>> 		LWLockRelease(SInvalReadLock);
>> 		return -1;
>> 	}
> 
> vs
> 
>> @@ -410,17 +398,16 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
>>  		/*
>>  		 * Insert new message(s) into proper slot of circular buffer
>>  		 */
>> -		max = segP->maxMsgNum;
>> +		max = pg_atomic_read_u32(&segP->maxMsgNum);
>>  		while (nthistime-- > 0)
>>  		{
>>  			segP->buffer[max % MAXNUMMESSAGES] = *data++;
>>  			max++;
>>  		}
>>
>> -		/* Update current value of maxMsgNum using spinlock */
>> -		SpinLockAcquire(&segP->msgnumLock);
>> -		segP->maxMsgNum = max;
>> -		SpinLockRelease(&segP->msgnumLock);
>> +		/* Update current value of maxMsgNum using atomic with memory barrier */
>> +		pg_write_barrier();
>> +		pg_atomic_write_u32(&segP->maxMsgNum, max);
>>
>>  		/*
>>  		 * Now that the maxMsgNum change is globally visible, we give everyone
>> 		 * a swift kick to make sure they read the newly added messages.
>> 		 * Releasing SInvalWriteLock will enforce a full memory barrier, so
>> 		 * these (unlocked) changes will be committed to memory before we exit
>> 		 * the function.
>> 		 */
>> 		for (i = 0; i < segP->numProcs; i++)
>> 		{
>> 			ProcState  *stateP = &segP->procState[segP->pgprocnos[i]];
>>
>> 			stateP->hasMessages = true;
>> 		}
> 
> On a loosely ordered architecture, the hasMessage=false in SIGetDataEntries()
> could be reordered with the read of maxMsgNum. Which, afaict, would lead to
> missing messages. That's not prevented by the pg_write_barrier() in
> SIInsertDataEntries().  I think there may be other similar dangers.
> 
> This could be solved by adding full memory barriers in a few places.

It is quite interesting remark, because SpinLockAcquire may be implemented
as `__sync_lock_test_and_set(lock, 1)` [1] on ARM/ARM64, and
`__sync_lock_test_and_set` has only "acquire barrier" semantic as GCC's
documentation says [2] . More over, `__sync_lock_release` used in this case
also provides only "release barrier" semantic.

Therefore, concerning these facts, current code state also doesn't give
guarantee `stateP->hasMessage = false` will not be reordered with `max =
segP->maxMsgNum`. Nor following read of messages are guaranteed to happen
after `max = segP->maxMsgNum`.

Given your expectations for SpinLockAcquire and SpinLockRelease to be full
memory barriers, and probably numerous usages of them as such, their
current implementation on ARM/ARM64 looks to be completely broken, imho.

[1]
https://github.com/postgres/postgres/blob/b3754dcc9ff7aba2268e98ecf4b5546353305cd2/src/include/stora...

[2]
https://gcc.gnu.org/onlinedocs/gcc-15.1.0/gcc/_005f_005fsync-Builtins.html#index-_005f_005fsync_005f...

-- 
regards
Yura Sokolov aka funny-falcon





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

* Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
@ 2025-06-02 18:20           ` Yura Sokolov <[email protected]>
  2025-06-03 21:04             ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-06-02 18:20 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Good day, Andres.
Good day, hackers.

05.05.2025 10:30, Yura Sokolov wrote:
> 21.03.2025 19:33, Andres Freund пишет:
>> Hi,
>>
>> On 2025-03-21 14:35:16 +0300, Yura Sokolov wrote:
>>> From 080c9e0de5e6e10751347e1ff50b65df424744cb Mon Sep 17 00:00:00 2001
>>> From: Yura Sokolov <[email protected]>
>>> Date: Mon, 3 Feb 2025 11:58:33 +0300
>>> Subject: [PATCH v2] sinvaladt.c: use atomic operations on maxMsgNum
>>>
>>> msgnumLock spinlock could be highly contended.
>>> Comment states it was used as memory barrier.
>>> Lets use atomic ops with memory barriers directly instead.
>>>
>>> Note: patch uses pg_read_barrier()/pg_write_barrier() instead of
>>> pg_atomic_read_membarrier_u32()/pg_atomic_write_membarrier_u32() since
>>> no full barrier semantic is required, and explicit read/write barriers
>>> are cheaper at least on x86_64.
>>
>> Is it actually true that full barriers aren't required?  I think we might
>> actually rely on a stronger ordering.
>>
>>
>>> @@ -506,10 +493,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
>>>  	 */
>>>  	stateP->hasMessages = false;
>>>
>>> -	/* Fetch current value of maxMsgNum using spinlock */
>>> -	SpinLockAcquire(&segP->msgnumLock);
>>> -	max = segP->maxMsgNum;
>>> -	SpinLockRelease(&segP->msgnumLock);
>>> +	/* Fetch current value of maxMsgNum using atomic with memory barrier */
>>> +	max = pg_atomic_read_u32(&segP->maxMsgNum);
>>> +	pg_read_barrier();
>>>
>>>  	if (stateP->resetState)
>>>  	{
>>> 		/*
>>> 		 * Force reset.  We can say we have dealt with any messages added
>>> 		 * since the reset, as well; and that means we should clear the
>>> 		 * signaled flag, too.
>>> 		 */
>>> 		stateP->nextMsgNum = max;
>>> 		stateP->resetState = false;
>>> 		stateP->signaled = false;
>>> 		LWLockRelease(SInvalReadLock);
>>> 		return -1;
>>> 	}
>>
>> vs
>>
>>> @@ -410,17 +398,16 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
>>>  		/*
>>>  		 * Insert new message(s) into proper slot of circular buffer
>>>  		 */
>>> -		max = segP->maxMsgNum;
>>> +		max = pg_atomic_read_u32(&segP->maxMsgNum);
>>>  		while (nthistime-- > 0)
>>>  		{
>>>  			segP->buffer[max % MAXNUMMESSAGES] = *data++;
>>>  			max++;
>>>  		}
>>>
>>> -		/* Update current value of maxMsgNum using spinlock */
>>> -		SpinLockAcquire(&segP->msgnumLock);
>>> -		segP->maxMsgNum = max;
>>> -		SpinLockRelease(&segP->msgnumLock);
>>> +		/* Update current value of maxMsgNum using atomic with memory barrier */
>>> +		pg_write_barrier();
>>> +		pg_atomic_write_u32(&segP->maxMsgNum, max);
>>>
>>>  		/*
>>>  		 * Now that the maxMsgNum change is globally visible, we give everyone
>>> 		 * a swift kick to make sure they read the newly added messages.
>>> 		 * Releasing SInvalWriteLock will enforce a full memory barrier, so
>>> 		 * these (unlocked) changes will be committed to memory before we exit
>>> 		 * the function.
>>> 		 */
>>> 		for (i = 0; i < segP->numProcs; i++)
>>> 		{
>>> 			ProcState  *stateP = &segP->procState[segP->pgprocnos[i]];
>>>
>>> 			stateP->hasMessages = true;
>>> 		}
>>
>> On a loosely ordered architecture, the hasMessage=false in SIGetDataEntries()
>> could be reordered with the read of maxMsgNum. Which, afaict, would lead to
>> missing messages. That's not prevented by the pg_write_barrier() in
>> SIInsertDataEntries().  I think there may be other similar dangers.
>>
>> This could be solved by adding full memory barriers in a few places.
> 
> It is quite interesting remark, because SpinLockAcquire may be implemented
> as `__sync_lock_test_and_set(lock, 1)` [1] on ARM/ARM64, and
> `__sync_lock_test_and_set` has only "acquire barrier" semantic as GCC's
> documentation says [2] . More over, `__sync_lock_release` used in this case
> also provides only "release barrier" semantic.
> 
> Therefore, concerning these facts, current code state also doesn't give
> guarantee `stateP->hasMessage = false` will not be reordered with `max =
> segP->maxMsgNum`. Nor following read of messages are guaranteed to happen
> after `max = segP->maxMsgNum`.
> 
> Given your expectations for SpinLockAcquire and SpinLockRelease to be full
> memory barriers, and probably numerous usages of them as such, their
> current implementation on ARM/ARM64 looks to be completely broken, imho.

I recognize my mistake and apologize for noise.

I'll try to describe how I understand it now.
There 4 operations done in two threads:
1m. write maxMsgNum
1h. set hasMessages
2h. unset hasMessages
2m. read maxMsgNum

Synchronization should forbid following orders of operations:
- 1h, 2h, 2m, 1m
- 1h, 2m, 1m, 2h
- 1h, 2m, 2h, 1m
- 2m, 1m, 1h, 2h
- 2m, 1h, 1m, 2h
- 2m, 1h, 2h, 1m
In other words: if read of maxMsgNum (2m) happened before write of
maxMsgNum (1m), then unset hasMessages (2h) should not happen after set of
hasMessages (1h).

Given 1m and 2m are wrapped in spin lock:
- while 1h could be reordered before 1m (since SpinLockRelease is just
release barrier), it could not be reordered before SpinLockAcquire (because
it is acquire barrier).
- same way while 2h could be reordered after 2m , it could not be reordered
with SpinLockRelease.

Introducing acquire and release operations, wrong orders above looks like:
- 1a( 1h, 2a{ 2h, 2m 2r}, 1m 1r)
- 1a( 1h, 2a{ 2m, 1m 1r), 2m 2r}
- 1a( 1h, 2a{ 2m, 2h 2r}, 1m 1r)
- 2a{ 2m, 1a( 1m, 1h 1r), 2h 2r}
- 2a{ 2m, 1a( 1h, 1m 1r), 2h 2r}
- 2a{ 2m, 1a( 1h, 2h 2r}, 1m 1r)
Therefore it is clear all wrong orders are forbidden by the spin lock
despite SpinLockAcquire and SpinLockRelease provides only acquire and
release semantics respectively.

Excuse me once again for misunderstanding this at first time.

But still problem of spin lock contention is here. And we found another one
place which have similar problem: numerous readers fights for single
slock_t slowing down them selves and writer. It because of
XLogRecoveryCtlData->info_lck. When there are a lot of logical/physical
walsenders, it could become noticeable bottleneck.
Part of this bottleneck were due to incorrect logical WAL senders target,
and were fixed in commit 5231ed82 [1]. But still there are measurable
speedup from mitigating spin lock contention at this place.

So I propose to introduce another spin lock type capable for Exclusive and
Shared lock modes (i.e. Write/Read modes) and use it in this two places.

I've tried to implement it with a bits of fairness: both readers and
writers attempters have "waiter" state, which blocks other side. Ie if
reader entered "waiter" state, it blocks concurrent writers (both fresh and
those in "waiter" state). But if writer entered "waiter" state, it blocks
fresh readers (but doesn't block readers, who entered "waiter" state).
Algorithm is self-made, I didn't found exactly same algorithm in the wild.

Patch is attached.

[1]
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=5231ed8262c94936a69bce41f64076630bbd9...


PS. Another possibility would be to use versionned optimistic lock:
- lock will be implemented as an pg_atomic_uint64 "version" storage,
- writer increments it both in LockWrite and in UnlockWrite. Therefore lock
is locked for write if version is odd.
- reader performs following loop:
-- wait untils version is even and remember it
-- perform read operations
-- if version didn't change, read is successful. Otherwise repeat loop.

Benefit of this approach: reader doesn't perform write to shared memory at all.

But it should use memory barriers before and after read loop to immitate
SpinLock semantic. And since we have no separate "acquire" and "release"
memory fences, it have to be pg_memory_barrier(). Or we should introduce
"acq/rel" fences

Other gotchas is only simple read operations allowed since data could be
changed while they are read (both found usecases satisfies this condition).
And read operations are repeated in a loop until version didn't change
around, so some kind of syntax sugar should be introduced.

This is well known approach, but I don't remember exact name and could not
google it fast.

-- 
regards
Yura Sokolov aka funny-falcon

Attachments:

  [text/x-patch] v4-0001-Exclusive-Shared-Read-Write-spin-lock.patch (17.8K, ../../[email protected]/2-v4-0001-Exclusive-Shared-Read-Write-spin-lock.patch)
  download | inline diff:
From 4afb4f24413352d7911c094481341ddb2663e73c Mon Sep 17 00:00:00 2001
From: Yura Sokolov <[email protected]>
Date: Mon, 2 Jun 2025 19:26:20 +0300
Subject: [PATCH v4] Exclusive-Shared (Read-Write) spin lock.

There are couple of places where spin lock is used just to separate
readers and writers. And there are a lot of readers and one or few writers.
Those places are source of contention in a huge system setups because
readers create bottleneck on always exclusive slock_t.

Mitigate it by introducing read-write spin lock.
Algorithm provides some level of fairness i.e. readers will allow waiting
writer to deterministically acquire lock. Writers also respect those
readers, who already entered logical "waiting" state.

Use this RWSpin lock in sending invalidations and notifies of walsenders.
---
 src/backend/access/transam/xlogrecovery.c |  70 ++++++-------
 src/backend/storage/ipc/sinvaladt.c       |  13 +--
 src/backend/storage/lmgr/Makefile         |   1 +
 src/backend/storage/lmgr/meson.build      |   1 +
 src/backend/storage/lmgr/rwspin.c         |  68 ++++++++++++
 src/include/storage/rwspin.h              | 122 ++++++++++++++++++++++
 src/tools/pgindent/typedefs.list          |   1 +
 7 files changed, 235 insertions(+), 41 deletions(-)
 create mode 100644 src/backend/storage/lmgr/rwspin.c
 create mode 100644 src/include/storage/rwspin.h

diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 6ce979f2d8b..73dbae51406 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -56,7 +56,7 @@
 #include "storage/latch.h"
 #include "storage/pmsignal.h"
 #include "storage/procarray.h"
-#include "storage/spin.h"
+#include "storage/rwspin.h"
 #include "utils/datetime.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc_hooks.h"
@@ -364,7 +364,7 @@ typedef struct XLogRecoveryCtlData
 	RecoveryPauseState recoveryPauseState;
 	ConditionVariable recoveryNotPausedCV;
 
-	slock_t		info_lck;		/* locks shared variables shown above */
+	RWSpin		info_lck;		/* locks shared variables shown above */
 } XLogRecoveryCtlData;
 
 static XLogRecoveryCtlData *XLogRecoveryCtl = NULL;
@@ -471,7 +471,7 @@ XLogRecoveryShmemInit(void)
 		return;
 	memset(XLogRecoveryCtl, 0, sizeof(XLogRecoveryCtlData));
 
-	SpinLockInit(&XLogRecoveryCtl->info_lck);
+	RWSpinInit(&XLogRecoveryCtl->info_lck);
 	InitSharedLatch(&XLogRecoveryCtl->recoveryWakeupLatch);
 	ConditionVariableInit(&XLogRecoveryCtl->recoveryNotPausedCV);
 }
@@ -1668,7 +1668,7 @@ PerformWalRecovery(void)
 	 * we had just replayed the record before the REDO location (or the
 	 * checkpoint record itself, if it's a shutdown checkpoint).
 	 */
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 	if (RedoStartLSN < CheckPointLoc)
 	{
 		XLogRecoveryCtl->lastReplayedReadRecPtr = InvalidXLogRecPtr;
@@ -1686,7 +1686,7 @@ PerformWalRecovery(void)
 	XLogRecoveryCtl->recoveryLastXTime = 0;
 	XLogRecoveryCtl->currentChunkStartTime = 0;
 	XLogRecoveryCtl->recoveryPauseState = RECOVERY_NOT_PAUSED;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 
 	/* Also ensure XLogReceiptTime has a sane value */
 	XLogReceiptTime = GetCurrentTimestamp();
@@ -1977,10 +1977,10 @@ ApplyWalRecord(XLogReaderState *xlogreader, XLogRecord *record, TimeLineID *repl
 	 * Update shared replayEndRecPtr before replaying this record, so that
 	 * XLogFlush will update minRecoveryPoint correctly.
 	 */
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->replayEndRecPtr = xlogreader->EndRecPtr;
 	XLogRecoveryCtl->replayEndTLI = *replayTLI;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 
 	/*
 	 * If we are attempting to enter Hot Standby mode, process XIDs we see
@@ -2014,11 +2014,11 @@ ApplyWalRecord(XLogReaderState *xlogreader, XLogRecord *record, TimeLineID *repl
 	 * Update lastReplayedEndRecPtr after this record has been successfully
 	 * replayed.
 	 */
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->lastReplayedReadRecPtr = xlogreader->ReadRecPtr;
 	XLogRecoveryCtl->lastReplayedEndRecPtr = xlogreader->EndRecPtr;
 	XLogRecoveryCtl->lastReplayedTLI = *replayTLI;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 
 	/* ------
 	 * Wakeup walsenders:
@@ -2269,9 +2269,9 @@ CheckRecoveryConsistency(void)
 		reachedConsistency &&
 		IsUnderPostmaster)
 	{
-		SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+		RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 		XLogRecoveryCtl->SharedHotStandbyActive = true;
-		SpinLockRelease(&XLogRecoveryCtl->info_lck);
+		RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 
 		LocalHotStandbyActive = true;
 
@@ -3082,9 +3082,9 @@ GetRecoveryPauseState(void)
 {
 	RecoveryPauseState state;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockSh(&XLogRecoveryCtl->info_lck);
 	state = XLogRecoveryCtl->recoveryPauseState;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockSh(&XLogRecoveryCtl->info_lck);
 
 	return state;
 }
@@ -3100,14 +3100,14 @@ GetRecoveryPauseState(void)
 void
 SetRecoveryPause(bool recoveryPause)
 {
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 
 	if (!recoveryPause)
 		XLogRecoveryCtl->recoveryPauseState = RECOVERY_NOT_PAUSED;
 	else if (XLogRecoveryCtl->recoveryPauseState == RECOVERY_NOT_PAUSED)
 		XLogRecoveryCtl->recoveryPauseState = RECOVERY_PAUSE_REQUESTED;
 
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 
 	if (!recoveryPause)
 		ConditionVariableBroadcast(&XLogRecoveryCtl->recoveryNotPausedCV);
@@ -3121,10 +3121,10 @@ static void
 ConfirmRecoveryPaused(void)
 {
 	/* If recovery pause is requested then set it paused */
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 	if (XLogRecoveryCtl->recoveryPauseState == RECOVERY_PAUSE_REQUESTED)
 		XLogRecoveryCtl->recoveryPauseState = RECOVERY_PAUSED;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 }
 
 
@@ -4421,9 +4421,9 @@ PromoteIsTriggered(void)
 	if (LocalPromoteIsTriggered)
 		return true;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockSh(&XLogRecoveryCtl->info_lck);
 	LocalPromoteIsTriggered = XLogRecoveryCtl->SharedPromoteIsTriggered;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockSh(&XLogRecoveryCtl->info_lck);
 
 	return LocalPromoteIsTriggered;
 }
@@ -4431,9 +4431,9 @@ PromoteIsTriggered(void)
 static void
 SetPromoteIsTriggered(void)
 {
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->SharedPromoteIsTriggered = true;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 
 	/*
 	 * Mark the recovery pause state as 'not paused' because the paused state
@@ -4531,9 +4531,9 @@ HotStandbyActive(void)
 	else
 	{
 		/* spinlock is essential on machines with weak memory ordering! */
-		SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+		RWSpinLockSh(&XLogRecoveryCtl->info_lck);
 		LocalHotStandbyActive = XLogRecoveryCtl->SharedHotStandbyActive;
-		SpinLockRelease(&XLogRecoveryCtl->info_lck);
+		RWSpinUnlockSh(&XLogRecoveryCtl->info_lck);
 
 		return LocalHotStandbyActive;
 	}
@@ -4561,10 +4561,10 @@ GetXLogReplayRecPtr(TimeLineID *replayTLI)
 	XLogRecPtr	recptr;
 	TimeLineID	tli;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockSh(&XLogRecoveryCtl->info_lck);
 	recptr = XLogRecoveryCtl->lastReplayedEndRecPtr;
 	tli = XLogRecoveryCtl->lastReplayedTLI;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockSh(&XLogRecoveryCtl->info_lck);
 
 	if (replayTLI)
 		*replayTLI = tli;
@@ -4584,10 +4584,10 @@ GetCurrentReplayRecPtr(TimeLineID *replayEndTLI)
 	XLogRecPtr	recptr;
 	TimeLineID	tli;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockSh(&XLogRecoveryCtl->info_lck);
 	recptr = XLogRecoveryCtl->replayEndRecPtr;
 	tli = XLogRecoveryCtl->replayEndTLI;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockSh(&XLogRecoveryCtl->info_lck);
 
 	if (replayEndTLI)
 		*replayEndTLI = tli;
@@ -4604,9 +4604,9 @@ GetCurrentReplayRecPtr(TimeLineID *replayEndTLI)
 static void
 SetLatestXTime(TimestampTz xtime)
 {
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->recoveryLastXTime = xtime;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 }
 
 /*
@@ -4617,9 +4617,9 @@ GetLatestXTime(void)
 {
 	TimestampTz xtime;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockSh(&XLogRecoveryCtl->info_lck);
 	xtime = XLogRecoveryCtl->recoveryLastXTime;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockSh(&XLogRecoveryCtl->info_lck);
 
 	return xtime;
 }
@@ -4633,9 +4633,9 @@ GetLatestXTime(void)
 static void
 SetCurrentChunkStartTime(TimestampTz xtime)
 {
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockEx(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->currentChunkStartTime = xtime;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockEx(&XLogRecoveryCtl->info_lck);
 }
 
 /*
@@ -4647,9 +4647,9 @@ GetCurrentChunkReplayStartTime(void)
 {
 	TimestampTz xtime;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWSpinLockSh(&XLogRecoveryCtl->info_lck);
 	xtime = XLogRecoveryCtl->currentChunkStartTime;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWSpinUnlockSh(&XLogRecoveryCtl->info_lck);
 
 	return xtime;
 }
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.c
index c5748b690f4..b8e1739a2eb 100644
--- a/src/backend/storage/ipc/sinvaladt.c
+++ b/src/backend/storage/ipc/sinvaladt.c
@@ -25,6 +25,7 @@
 #include "storage/shmem.h"
 #include "storage/sinvaladt.h"
 #include "storage/spin.h"
+#include "storage/rwspin.h"
 
 /*
  * Conceptually, the shared cache invalidation messages are stored in an
@@ -171,7 +172,7 @@ typedef struct SISeg
 	int			maxMsgNum;		/* next message number to be assigned */
 	int			nextThreshold;	/* # of messages to call SICleanupQueue */
 
-	slock_t		msgnumLock;		/* spinlock protecting maxMsgNum */
+	RWSpin		msgnumLock;		/* spinlock protecting maxMsgNum */
 
 	/*
 	 * Circular buffer holding shared-inval messages
@@ -246,7 +247,7 @@ SharedInvalShmemInit(void)
 	shmInvalBuffer->minMsgNum = 0;
 	shmInvalBuffer->maxMsgNum = 0;
 	shmInvalBuffer->nextThreshold = CLEANUP_MIN;
-	SpinLockInit(&shmInvalBuffer->msgnumLock);
+	RWSpinInit(&shmInvalBuffer->msgnumLock);
 
 	/* The buffer[] array is initially all unused, so we need not fill it */
 
@@ -419,9 +420,9 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		}
 
 		/* Update current value of maxMsgNum using spinlock */
-		SpinLockAcquire(&segP->msgnumLock);
+		RWSpinLockEx(&segP->msgnumLock);
 		segP->maxMsgNum = max;
-		SpinLockRelease(&segP->msgnumLock);
+		RWSpinUnlockEx(&segP->msgnumLock);
 
 		/*
 		 * Now that the maxMsgNum change is globally visible, we give everyone
@@ -508,9 +509,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 	stateP->hasMessages = false;
 
 	/* Fetch current value of maxMsgNum using spinlock */
-	SpinLockAcquire(&segP->msgnumLock);
+	RWSpinLockSh(&segP->msgnumLock);
 	max = segP->maxMsgNum;
-	SpinLockRelease(&segP->msgnumLock);
+	RWSpinUnlockSh(&segP->msgnumLock);
 
 	if (stateP->resetState)
 	{
diff --git a/src/backend/storage/lmgr/Makefile b/src/backend/storage/lmgr/Makefile
index 6cbaf23b855..c8f5147ae9e 100644
--- a/src/backend/storage/lmgr/Makefile
+++ b/src/backend/storage/lmgr/Makefile
@@ -21,6 +21,7 @@ OBJS = \
 	predicate.o \
 	proc.o \
 	s_lock.o \
+	rwspin.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git a/src/backend/storage/lmgr/meson.build b/src/backend/storage/lmgr/meson.build
index a5490c1047f..94e810885ed 100644
--- a/src/backend/storage/lmgr/meson.build
+++ b/src/backend/storage/lmgr/meson.build
@@ -9,4 +9,5 @@ backend_sources += files(
   'predicate.c',
   'proc.c',
   's_lock.c',
+  'rwspin.c',
 )
diff --git a/src/backend/storage/lmgr/rwspin.c b/src/backend/storage/lmgr/rwspin.c
new file mode 100644
index 00000000000..584249d5cb1
--- /dev/null
+++ b/src/backend/storage/lmgr/rwspin.c
@@ -0,0 +1,68 @@
+#include "storage/rwspin.h"
+#include "storage/s_lock.h"
+
+void
+RWSpinLockEx_slowpath(RWSpin *spin, uint32 val)
+{
+	SpinDelayStatus delay;
+
+	init_local_spin_delay(&delay);
+	do
+	{
+		if ((val & RW_SPIN_EX_WAIT) == 0)
+		{
+			/*
+			 * Enter ExWaiter state, so concurrent ShTriers will step down
+			 * their attempts.
+			 */
+			val = pg_atomic_fetch_or_u32(spin, RW_SPIN_EX_WAIT);
+			val |= RW_SPIN_EX_WAIT;
+		}
+		else
+		{
+			perform_spin_delay(&delay);
+			val = pg_atomic_read_u32(spin);
+		}
+	} while (val > RW_SPIN_EX_WAIT ||
+			 !pg_atomic_compare_exchange_u32(spin, &val, RW_SPIN_EX_LOCK));
+	finish_spin_delay(&delay);
+}
+
+void
+RWSpinLockSh_slowpath(RWSpin *spin, uint32 val)
+{
+	SpinDelayStatus delay;
+
+	init_local_spin_delay(&delay);
+
+	/* If there is ExWaiter, we have to step down and remain to be ShTrier. */
+	while ((val & RW_SPIN_EX_WAIT) != 0)
+	{
+		val = pg_atomic_sub_fetch_u32(spin, RW_SPIN_SH_MARK);
+		do
+		{
+			perform_spin_delay(&delay);
+			val = pg_atomic_read_u32(spin);
+		} while ((val & RW_SPIN_EX_WAIT) != 0);
+		/* ExWaiter disappeared, we may retry */
+		val = pg_atomic_add_fetch_u32(spin, RW_SPIN_SH_MARK);
+	}
+
+	/*
+	 * There is no ExWaiter, so we don't have to step down any more. If there
+	 * is ExHolder, then we've entered ShWaiter state and have to wait until
+	 * lock is released. Otherwise we are already ShLocker.
+	 */
+	while ((val & RW_SPIN_EX_LOCK) != 0)
+	{
+		perform_spin_delay(&delay);
+		val = pg_atomic_read_u32(spin);
+	}
+	finish_spin_delay(&delay);
+
+	/*
+	 * Atomics are tricky, and last loop performs only reads. So it is better
+	 * to place memory barrier here.
+	 */
+	pg_memory_barrier();
+}
diff --git a/src/include/storage/rwspin.h b/src/include/storage/rwspin.h
new file mode 100644
index 00000000000..1a7c5ee9c0f
--- /dev/null
+++ b/src/include/storage/rwspin.h
@@ -0,0 +1,122 @@
+/*-------------------------------------------------------------
+ *
+ * rwspin.h
+ *     Exclusive-Shared (Read-Write) capable spin lock implementation.
+ *
+ * Implementeds algorithm provides some level of fairness:
+ * - read attempters allows write waiters to lock the lock
+ * - write attempters allows read waiters to lock the lock.
+ * It is done through usage of intermediate "waiter" states.
+ *
+ * Guarantees and warnings are almost the same as for slock_t. Read them at
+ * spin.h
+ */
+
+#ifndef RWSPIN_H
+#define RWSPIN_H
+
+#include "c.h"
+#include "port/atomics.h"
+
+typedef pg_atomic_uint32 RWSpin;
+
+/* Initialize RWSpin. */
+static inline void RWSpinInit(RWSpin *spin);
+
+/* Acquire RWSpin in exclusive mode. */
+static inline void RWSpinLockEx(RWSpin *spin);
+
+/* Release exclusive lock of RWSpin. */
+static inline void RWSpinUnlockEx(RWSpin *spin);
+
+/* Acquire RWSpin in shared mode. */
+static inline void RWSpinLockSh(RWSpin *spin);
+
+/* Release shared lock of RWSpin. */
+static inline void RWSpinUnlockSh(RWSpin *spin);
+
+/* Implementation */
+
+/*
+ * Exclusive-Shared "fair" spin lock is implemented with following algorithm:
+ * - attempter could be in following states:
+ *   Exclusive Lock Trier  (ExTrier)
+ *   Exclusive Lock Waiter (ExWaiter)
+ *   Exclusive Lock Holder (ExHolder)
+ *   Shared Lock Trier     (ShTrier)
+ *   Shared Lock Waiter    (ShWaiter)
+ *   Shared Lock Holder    (ShHolder)
+ * - ExTrier can become ExHolder only if there are no any holder or ShWaiter.
+ *   Otherwise it marks existence of ExWaiter and become ExWaiter.
+ * - ExWaiter waits until there are no any holder and ShWaiter. Then it tries
+ *   to become ExHolder.
+ *   Note: ExWaiter clears its mark on acquiring lock. So other ExWaiters
+ *   effectively become ExTrier and have to reassign mark.
+ * - ShTrier increments shared lock marks.
+ *   If there are ExWaiter, it decrements shared lock marks and waits for
+ *   ExWaiter to dismissing. Then it retries.
+ *   If there are ExHolder, it becomes ShWaiter.
+ *   Otherwise it becomes ShHolder.
+ * - ShWaiter just waits until ExHolder will release the lock.
+ *	 Since ExWaiter will not switch to ExHolder in presence of ShWaiter,
+ *   ShWaiter need not to check for it.
+ *   When ExHolder release the lock all ShWaiters become ShHolder.
+ */
+
+static inline void
+RWSpinInit(RWSpin *spin)
+{
+	pg_atomic_init_u32(spin, 0);
+}
+
+enum
+{
+	RW_SPIN_EX_WAIT = 1,
+	RW_SPIN_EX_LOCK = 2,
+	RW_SPIN_EX_MASK = RW_SPIN_EX_WAIT | RW_SPIN_EX_LOCK,
+	RW_SPIN_SH_MARK = 4,
+};
+
+extern void RWSpinLockEx_slowpath(RWSpin *spin, uint32 val);
+extern void RWSpinLockSh_slowpath(RWSpin *spin, uint32 val);
+
+static inline void
+RWSpinLockEx(RWSpin *spin)
+{
+	uint32		old = pg_atomic_read_u32(spin);
+
+	if (old > RW_SPIN_EX_WAIT ||
+		!pg_atomic_compare_exchange_u32(spin, &old, RW_SPIN_EX_LOCK))
+		RWSpinLockEx_slowpath(spin, old);
+}
+
+static inline void
+RWSpinUnlockEx(RWSpin *spin)
+{
+	pg_atomic_sub_fetch_u32(spin, RW_SPIN_EX_LOCK);
+}
+
+static inline void
+RWSpinLockSh(RWSpin *spin)
+{
+	/*
+	 * Try to lock in share mode or mark as waiter for share lock. Both is
+	 * done with incrementing share mark count.
+	 */
+	uint32		val = pg_atomic_add_fetch_u32(spin, RW_SPIN_SH_MARK);
+
+	/*
+	 * If there are exclusive locker or waiter for exclusive lock, we have to
+	 * do more complex things.
+	 */
+	if (unlikely((val & RW_SPIN_EX_MASK) != 0))
+		RWSpinLockSh_slowpath(spin, val);
+}
+
+static inline void
+RWSpinUnlockSh(RWSpin *spin)
+{
+	pg_atomic_sub_fetch_u32(spin, RW_SPIN_SH_MARK);
+}
+
+#endif
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a8346cda633..37cd705fcf1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2399,6 +2399,7 @@ RTEPermissionInfo
 RWConflict
 RWConflictData
 RWConflictPoolHeader
+RWSpin
 Range
 RangeBound
 RangeBox
-- 
2.43.0



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

* Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
@ 2025-06-03 21:04             ` Andres Freund <[email protected]>
  2025-06-16 14:28               ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Andres Freund @ 2025-06-03 21:04 UTC (permalink / raw)
  To: Yura Sokolov <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Hi,

On 2025-06-02 21:20:33 +0300, Yura Sokolov wrote:
> But still problem of spin lock contention is here.

I still would like to see a reproducer for this.


> So I propose to introduce another spin lock type capable for Exclusive and
> Shared lock modes (i.e. Write/Read modes) and use it in this two places.

I am vehemently opposed to that. We should work towards getting rid of
spinlocks, not introduce more versions of spinlocks. Userspace spinlocks
largely are a bad idea. We should instead make properly queued locks cheaper
(e.g. by having an exclusive-only lock, which can be cheaper to release on
common platforms).

Greetings,

Andres Freund





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

* Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-03 21:04             ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
@ 2025-06-16 14:28               ` Yura Sokolov <[email protected]>
  2025-06-16 14:41                 ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-06-16 14:28 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

04.06.2025 00:04, Andres Freund пишет:
> Hi,
> 
> On 2025-06-02 21:20:33 +0300, Yura Sokolov wrote:
>> But still problem of spin lock contention is here.
> 
> I still would like to see a reproducer for this.

For problem in sinvaladt.c we have no synthetic reproducer. But version
with changed maxMsgNum to atomic solved customer's issue.

For problem on XLogRecoveryCtlData->info_lck we have reproducer which shows
follower with a lot of logical walsenders with patched version lags less
and uses less CPU... but only 4-socket Xeon(R) Gold 6348H. There is no
benefit from patch on 2-socket Intel(R) Xeon(R) Gold 5220R.

>> So I propose to introduce another spin lock type capable for Exclusive and
>> Shared lock modes (i.e. Write/Read modes) and use it in this two places.
> 
> I am vehemently opposed to that. We should work towards getting rid of
> spinlocks, not introduce more versions of spinlocks. Userspace spinlocks
> largely are a bad idea. We should instead make properly queued locks cheaper
> (e.g. by having an exclusive-only lock, which can be cheaper to release on
> common platforms).

1. when faster lock will arrive?
2. "exclusive-only" lock will never scale at scenario when there are few
writers and a lot of readers. It is just direct consequence of Amdahl's Law.


To be honestly, our version of XLogRecoveryCtlData->info_lck fix is based
on optimistic read-write lock. I've tried to make more regular read-write
spin lock in previous patch version to make it more familiar.

But after playing a bit with variants using simple C program [1], I found
optimistic lock is only really scalable solution for the problem (aside of
direct use of atomic operations).

So attached version contains optimistic read-write lock used in these two
places.

[1] https://pastebin.com/CwSPkeGi

-- 
regards
Yura Sokolov aka funny-falcon

Attachments:

  [text/x-patch] v5-0001-Read-Write-optimistic-spin-lock.patch (17.6K, ../../[email protected]/2-v5-0001-Read-Write-optimistic-spin-lock.patch)
  download | inline diff:
From 04ca9870a9bb1036aede6ee32b616908a065341a Mon Sep 17 00:00:00 2001
From: Yura Sokolov <[email protected]>
Date: Mon, 2 Jun 2025 19:26:20 +0300
Subject: [PATCH v5] Read-Write optimistic spin lock.

There are couple of places where spin lock is used just to separate
readers and writers. And there are a lot of readers and one or few writers.
Those places are source of contention in a huge system setups because
readers create bottleneck on always exclusive slock_t.

Mitigate it by introducing read-write optimistic spin lock.
Read is performed without any write operation on lock itself. Correctness
of read is checked by comparing lock version before and after read.

Use this RWSpin lock in sending invalidations and notifies of walsenders.

If no native 64bit atomic support present, fallback to regular spin lock.
---
 src/backend/access/transam/xlogrecovery.c |  70 +++++------
 src/backend/storage/ipc/sinvaladt.c       |  14 +--
 src/backend/storage/lmgr/Makefile         |   1 +
 src/backend/storage/lmgr/meson.build      |   1 +
 src/backend/storage/lmgr/rwoptspin.c      |  33 +++++
 src/include/storage/rwoptspin.h           | 139 ++++++++++++++++++++++
 src/tools/pgindent/typedefs.list          |   1 +
 7 files changed, 217 insertions(+), 42 deletions(-)
 create mode 100644 src/backend/storage/lmgr/rwoptspin.c
 create mode 100644 src/include/storage/rwoptspin.h

diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 6ce979f2d8b..2c651d7b83d 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -56,7 +56,7 @@
 #include "storage/latch.h"
 #include "storage/pmsignal.h"
 #include "storage/procarray.h"
-#include "storage/spin.h"
+#include "storage/rwoptspin.h"
 #include "utils/datetime.h"
 #include "utils/fmgrprotos.h"
 #include "utils/guc_hooks.h"
@@ -364,7 +364,7 @@ typedef struct XLogRecoveryCtlData
 	RecoveryPauseState recoveryPauseState;
 	ConditionVariable recoveryNotPausedCV;
 
-	slock_t		info_lck;		/* locks shared variables shown above */
+	RWOptSpin	info_lck;		/* locks shared variables shown above */
 } XLogRecoveryCtlData;
 
 static XLogRecoveryCtlData *XLogRecoveryCtl = NULL;
@@ -471,7 +471,7 @@ XLogRecoveryShmemInit(void)
 		return;
 	memset(XLogRecoveryCtl, 0, sizeof(XLogRecoveryCtlData));
 
-	SpinLockInit(&XLogRecoveryCtl->info_lck);
+	RWOptSpinInit(&XLogRecoveryCtl->info_lck);
 	InitSharedLatch(&XLogRecoveryCtl->recoveryWakeupLatch);
 	ConditionVariableInit(&XLogRecoveryCtl->recoveryNotPausedCV);
 }
@@ -1668,7 +1668,7 @@ PerformWalRecovery(void)
 	 * we had just replayed the record before the REDO location (or the
 	 * checkpoint record itself, if it's a shutdown checkpoint).
 	 */
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 	if (RedoStartLSN < CheckPointLoc)
 	{
 		XLogRecoveryCtl->lastReplayedReadRecPtr = InvalidXLogRecPtr;
@@ -1686,7 +1686,7 @@ PerformWalRecovery(void)
 	XLogRecoveryCtl->recoveryLastXTime = 0;
 	XLogRecoveryCtl->currentChunkStartTime = 0;
 	XLogRecoveryCtl->recoveryPauseState = RECOVERY_NOT_PAUSED;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 
 	/* Also ensure XLogReceiptTime has a sane value */
 	XLogReceiptTime = GetCurrentTimestamp();
@@ -1977,10 +1977,10 @@ ApplyWalRecord(XLogReaderState *xlogreader, XLogRecord *record, TimeLineID *repl
 	 * Update shared replayEndRecPtr before replaying this record, so that
 	 * XLogFlush will update minRecoveryPoint correctly.
 	 */
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->replayEndRecPtr = xlogreader->EndRecPtr;
 	XLogRecoveryCtl->replayEndTLI = *replayTLI;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 
 	/*
 	 * If we are attempting to enter Hot Standby mode, process XIDs we see
@@ -2014,11 +2014,11 @@ ApplyWalRecord(XLogReaderState *xlogreader, XLogRecord *record, TimeLineID *repl
 	 * Update lastReplayedEndRecPtr after this record has been successfully
 	 * replayed.
 	 */
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->lastReplayedReadRecPtr = xlogreader->ReadRecPtr;
 	XLogRecoveryCtl->lastReplayedEndRecPtr = xlogreader->EndRecPtr;
 	XLogRecoveryCtl->lastReplayedTLI = *replayTLI;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 
 	/* ------
 	 * Wakeup walsenders:
@@ -2269,9 +2269,9 @@ CheckRecoveryConsistency(void)
 		reachedConsistency &&
 		IsUnderPostmaster)
 	{
-		SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+		RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 		XLogRecoveryCtl->SharedHotStandbyActive = true;
-		SpinLockRelease(&XLogRecoveryCtl->info_lck);
+		RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 
 		LocalHotStandbyActive = true;
 
@@ -3082,9 +3082,9 @@ GetRecoveryPauseState(void)
 {
 	RecoveryPauseState state;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadDo(&XLogRecoveryCtl->info_lck);
 	state = XLogRecoveryCtl->recoveryPauseState;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadWhile(&XLogRecoveryCtl->info_lck);
 
 	return state;
 }
@@ -3100,14 +3100,14 @@ GetRecoveryPauseState(void)
 void
 SetRecoveryPause(bool recoveryPause)
 {
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 
 	if (!recoveryPause)
 		XLogRecoveryCtl->recoveryPauseState = RECOVERY_NOT_PAUSED;
 	else if (XLogRecoveryCtl->recoveryPauseState == RECOVERY_NOT_PAUSED)
 		XLogRecoveryCtl->recoveryPauseState = RECOVERY_PAUSE_REQUESTED;
 
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 
 	if (!recoveryPause)
 		ConditionVariableBroadcast(&XLogRecoveryCtl->recoveryNotPausedCV);
@@ -3121,10 +3121,10 @@ static void
 ConfirmRecoveryPaused(void)
 {
 	/* If recovery pause is requested then set it paused */
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 	if (XLogRecoveryCtl->recoveryPauseState == RECOVERY_PAUSE_REQUESTED)
 		XLogRecoveryCtl->recoveryPauseState = RECOVERY_PAUSED;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 }
 
 
@@ -4421,9 +4421,9 @@ PromoteIsTriggered(void)
 	if (LocalPromoteIsTriggered)
 		return true;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadDo(&XLogRecoveryCtl->info_lck);
 	LocalPromoteIsTriggered = XLogRecoveryCtl->SharedPromoteIsTriggered;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadWhile(&XLogRecoveryCtl->info_lck);
 
 	return LocalPromoteIsTriggered;
 }
@@ -4431,9 +4431,9 @@ PromoteIsTriggered(void)
 static void
 SetPromoteIsTriggered(void)
 {
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->SharedPromoteIsTriggered = true;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 
 	/*
 	 * Mark the recovery pause state as 'not paused' because the paused state
@@ -4531,9 +4531,9 @@ HotStandbyActive(void)
 	else
 	{
 		/* spinlock is essential on machines with weak memory ordering! */
-		SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+		RWOptSpinReadDo(&XLogRecoveryCtl->info_lck);
 		LocalHotStandbyActive = XLogRecoveryCtl->SharedHotStandbyActive;
-		SpinLockRelease(&XLogRecoveryCtl->info_lck);
+		RWOptSpinReadWhile(&XLogRecoveryCtl->info_lck);
 
 		return LocalHotStandbyActive;
 	}
@@ -4561,10 +4561,10 @@ GetXLogReplayRecPtr(TimeLineID *replayTLI)
 	XLogRecPtr	recptr;
 	TimeLineID	tli;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadDo(&XLogRecoveryCtl->info_lck);
 	recptr = XLogRecoveryCtl->lastReplayedEndRecPtr;
 	tli = XLogRecoveryCtl->lastReplayedTLI;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadWhile(&XLogRecoveryCtl->info_lck);
 
 	if (replayTLI)
 		*replayTLI = tli;
@@ -4584,10 +4584,10 @@ GetCurrentReplayRecPtr(TimeLineID *replayEndTLI)
 	XLogRecPtr	recptr;
 	TimeLineID	tli;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadDo(&XLogRecoveryCtl->info_lck);
 	recptr = XLogRecoveryCtl->replayEndRecPtr;
 	tli = XLogRecoveryCtl->replayEndTLI;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadWhile(&XLogRecoveryCtl->info_lck);
 
 	if (replayEndTLI)
 		*replayEndTLI = tli;
@@ -4604,9 +4604,9 @@ GetCurrentReplayRecPtr(TimeLineID *replayEndTLI)
 static void
 SetLatestXTime(TimestampTz xtime)
 {
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->recoveryLastXTime = xtime;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 }
 
 /*
@@ -4617,9 +4617,9 @@ GetLatestXTime(void)
 {
 	TimestampTz xtime;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadDo(&XLogRecoveryCtl->info_lck);
 	xtime = XLogRecoveryCtl->recoveryLastXTime;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadWhile(&XLogRecoveryCtl->info_lck);
 
 	return xtime;
 }
@@ -4633,9 +4633,9 @@ GetLatestXTime(void)
 static void
 SetCurrentChunkStartTime(TimestampTz xtime)
 {
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinAcquire(&XLogRecoveryCtl->info_lck);
 	XLogRecoveryCtl->currentChunkStartTime = xtime;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinRelease(&XLogRecoveryCtl->info_lck);
 }
 
 /*
@@ -4647,9 +4647,9 @@ GetCurrentChunkReplayStartTime(void)
 {
 	TimestampTz xtime;
 
-	SpinLockAcquire(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadDo(&XLogRecoveryCtl->info_lck);
 	xtime = XLogRecoveryCtl->currentChunkStartTime;
-	SpinLockRelease(&XLogRecoveryCtl->info_lck);
+	RWOptSpinReadWhile(&XLogRecoveryCtl->info_lck);
 
 	return xtime;
 }
diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.c
index c5748b690f4..cbc21667f26 100644
--- a/src/backend/storage/ipc/sinvaladt.c
+++ b/src/backend/storage/ipc/sinvaladt.c
@@ -24,7 +24,7 @@
 #include "storage/procsignal.h"
 #include "storage/shmem.h"
 #include "storage/sinvaladt.h"
-#include "storage/spin.h"
+#include "storage/rwoptspin.h"
 
 /*
  * Conceptually, the shared cache invalidation messages are stored in an
@@ -171,7 +171,7 @@ typedef struct SISeg
 	int			maxMsgNum;		/* next message number to be assigned */
 	int			nextThreshold;	/* # of messages to call SICleanupQueue */
 
-	slock_t		msgnumLock;		/* spinlock protecting maxMsgNum */
+	RWOptSpin	msgnumLock;		/* spinlock protecting maxMsgNum */
 
 	/*
 	 * Circular buffer holding shared-inval messages
@@ -246,7 +246,7 @@ SharedInvalShmemInit(void)
 	shmInvalBuffer->minMsgNum = 0;
 	shmInvalBuffer->maxMsgNum = 0;
 	shmInvalBuffer->nextThreshold = CLEANUP_MIN;
-	SpinLockInit(&shmInvalBuffer->msgnumLock);
+	RWOptSpinInit(&shmInvalBuffer->msgnumLock);
 
 	/* The buffer[] array is initially all unused, so we need not fill it */
 
@@ -419,9 +419,9 @@ SIInsertDataEntries(const SharedInvalidationMessage *data, int n)
 		}
 
 		/* Update current value of maxMsgNum using spinlock */
-		SpinLockAcquire(&segP->msgnumLock);
+		RWOptSpinAcquire(&segP->msgnumLock);
 		segP->maxMsgNum = max;
-		SpinLockRelease(&segP->msgnumLock);
+		RWOptSpinRelease(&segP->msgnumLock);
 
 		/*
 		 * Now that the maxMsgNum change is globally visible, we give everyone
@@ -508,9 +508,9 @@ SIGetDataEntries(SharedInvalidationMessage *data, int datasize)
 	stateP->hasMessages = false;
 
 	/* Fetch current value of maxMsgNum using spinlock */
-	SpinLockAcquire(&segP->msgnumLock);
+	RWOptSpinReadDo(&segP->msgnumLock);
 	max = segP->maxMsgNum;
-	SpinLockRelease(&segP->msgnumLock);
+	RWOptSpinReadWhile(&segP->msgnumLock);
 
 	if (stateP->resetState)
 	{
diff --git a/src/backend/storage/lmgr/Makefile b/src/backend/storage/lmgr/Makefile
index 6cbaf23b855..89892835b97 100644
--- a/src/backend/storage/lmgr/Makefile
+++ b/src/backend/storage/lmgr/Makefile
@@ -21,6 +21,7 @@ OBJS = \
 	predicate.o \
 	proc.o \
 	s_lock.o \
+	rwoptspin.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git a/src/backend/storage/lmgr/meson.build b/src/backend/storage/lmgr/meson.build
index a5490c1047f..2f940931961 100644
--- a/src/backend/storage/lmgr/meson.build
+++ b/src/backend/storage/lmgr/meson.build
@@ -9,4 +9,5 @@ backend_sources += files(
   'predicate.c',
   'proc.c',
   's_lock.c',
+  'rwoptspin.c',
 )
diff --git a/src/backend/storage/lmgr/rwoptspin.c b/src/backend/storage/lmgr/rwoptspin.c
new file mode 100644
index 00000000000..7a8e7065d07
--- /dev/null
+++ b/src/backend/storage/lmgr/rwoptspin.c
@@ -0,0 +1,33 @@
+#include "storage/rwoptspin.h"
+
+#if PG_RWOPTSPIN_NATIVE
+
+#include "storage/s_lock.h"
+void
+RWOptSpinAcquire_slowpath(RWOptSpin *spin, const char *file, int line, const char *func)
+{
+	SpinDelayStatus delay;
+
+	init_spin_delay(&delay, file, line, func);
+	do
+	{
+		perform_spin_delay(&delay);
+	} while ((pg_atomic_read_u64(spin) & 1) != 0 ||
+			 ((pg_atomic_fetch_or_u64(spin, 1) & 1) != 0));
+	finish_spin_delay(&delay);
+}
+
+void
+RWOptSpinRead_wait(RWOptSpin *spin, uint64 *version, const char *file, int line, const char *func)
+{
+	SpinDelayStatus delay;
+
+	init_spin_delay(&delay, file, line, func);
+	do
+	{
+		perform_spin_delay(&delay);
+		*version = pg_atomic_read_u64(spin);
+	} while (*version & 1);
+	finish_spin_delay(&delay);
+}
+#endif
diff --git a/src/include/storage/rwoptspin.h b/src/include/storage/rwoptspin.h
new file mode 100644
index 00000000000..d806ea82a4d
--- /dev/null
+++ b/src/include/storage/rwoptspin.h
@@ -0,0 +1,139 @@
+/*-------------------------------------------------------------
+ *
+ * rwoptspin.h
+ *     Read-Write optimistic spin lock.
+ *
+ * It works best when there are few writers and a lot of simultaneous
+ * readers.
+ *
+ * Synchronization primitive relied on lock version:
+ * - to acquire write lock, writer first makes version odd with atomic
+ * operation (and spins if it is already odd),
+ * - to release write lock, writer increments version, therefore version become
+ * even,
+ * - reader ensures version is even (spin-waiting in other case), then reads
+ * protected values "lockless".
+ * - after read version should be rechecked. If it changed, read should be
+ * retried.
+ *
+ * Reader could perform only simple read operations. It should not perform any
+ * write operation in its loop nor complex reads which may be invalid in case
+ * of concurrent changes.
+ */
+
+#ifndef RWSPIN_H
+#define RWSPIN_H
+
+#include "c.h"
+#include "port/atomics.h"
+
+#if defined(PG_HAVE_ATOMIC_U64_SUPPORT) && !defined(PG_HAVE_ATOMIC_U64_SIMULATION)
+#define PG_RWOPTSPIN_NATIVE 1
+
+typedef pg_atomic_uint64 RWOptSpin;
+
+/* Initialize RWSpin. */
+static inline void RWOptSpinInit(RWOptSpin *spin);
+
+/* Acquire RWOptSpin for write operation. */
+#define RWOptSpinAcquire(spin) RWOptSpinAcquire_impl((spin), __FILE__, __LINE__, __func__)
+
+/* Release write lock of RWOptSpin. */
+static inline void RWOptSpinRelease(RWOptSpin *spin);
+
+/*-----------------------------------------
+ * Syntax sugar for read operations.
+ * Example usage:
+ *
+ *    RWOptSpinReadDo(rwspin);
+ *    val1 = read_protected_value1();
+ *    val2 = read_protected_value2();
+ *    RWOptSpinReadWhile(rwspin);
+ */
+#define RWOptSpinReadDo(rwspin) \
+	{ uint64 rwopt_version = RWOptSpinReadStart((rwspin), __FILE__, __LINE__, __func__); do {
+#define RWOptSpinReadWhile(rwspin) \
+	} while (RWOptSpinRetryRead((rwspin), &rwopt_version, __FILE__, __LINE__, __func__)); }
+
+/* Implementation */
+
+static inline void
+RWOptSpinInit(RWOptSpin *spin)
+{
+	pg_atomic_init_u64(spin, 0);
+}
+
+extern void RWOptSpinAcquire_slowpath(RWOptSpin *spin, const char *file, int line, const char *func);
+
+static inline void
+RWOptSpinAcquire_impl(RWOptSpin *spin, const char *file, int line, const char *func)
+{
+	if (likely((pg_atomic_fetch_or_u64(spin, 1) & 1) == 0))
+		return;
+	RWOptSpinAcquire_slowpath(spin, file, line, func);
+}
+
+static inline void
+RWOptSpinRelease(RWOptSpin *spin)
+{
+	pg_atomic_add_fetch_u64(spin, 1);
+}
+
+extern void RWOptSpinRead_wait(RWOptSpin *spin, uint64 *version, const char *file, int line, const char *func);
+
+static inline uint64
+RWOptSpinReadStart(RWOptSpin *spin, const char *file, int line, const char *func)
+{
+	uint64		version = pg_atomic_read_u64(spin);
+
+	/* If it is write locked, wait until writer finished its work */
+	if (version & 1)
+		RWOptSpinRead_wait(spin, &version, file, line, func);
+
+	/*
+	 * Memory barrier is to provide both acquire+release semantic and read
+	 * barrier between version read and actual reads.
+	 */
+	pg_memory_barrier();
+
+	return version;
+}
+
+static inline bool
+RWOptSpinRetryRead(RWOptSpin *spin, uint64 *version, const char *file, int line, const char *func)
+{
+	uint64		cur;
+
+	pg_read_barrier();
+	cur = pg_atomic_read_u64(spin);
+	if (cur == *version)
+		return false;
+
+	*version = cur;
+
+	/* If it is write locked, wait until writer finished its work */
+	if (*version & 1)
+		RWOptSpinRead_wait(spin, version, file, line, func);
+
+	pg_read_barrier();
+	return true;
+}
+
+#else							/* defined(PG_HAVE_ATOMIC_U64_SUPPORT) &&
+								 * !defined(PG_HAVE_ATOMIC_U64_SIMULATION) */
+
+#include "storage/spin.h"
+
+typedef slock_t RWOptSpin;
+
+#define RWOptSpinInit(lock) SpinLockInit(lock)
+#define RWOptSpinAcquire(lock) SpinLockAcquire(lock)
+#define RWOptSpinRelease(lock) SpinLockRelease(lock)
+
+#define RWOptSpinReadDo(lock) SpinLockAcquire(lock)
+#define RWOptSpinReadWhile(lock) SpinLockRelease(lock)
+
+#endif							/* defined(PG_HAVE_ATOMIC_U64_SUPPORT) &&
+								 * !defined(PG_HAVE_ATOMIC_U64_SIMULATION) */
+
+#endif
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 32d6e718adc..1b9037b420d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2403,6 +2403,7 @@ RTEPermissionInfo
 RWConflict
 RWConflictData
 RWConflictPoolHeader
+RWOptSpin
 Range
 RangeBound
 RangeBox
-- 
2.43.0



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

* Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-03 21:04             ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-06-16 14:28               ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
@ 2025-06-16 14:41                 ` Andres Freund <[email protected]>
  2025-06-25 13:41                   ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Sergey Shinderuk <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Andres Freund @ 2025-06-16 14:41 UTC (permalink / raw)
  To: Yura Sokolov <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Hi,

On 2025-06-16 17:28:31 +0300, Yura Sokolov wrote:
> 04.06.2025 00:04, Andres Freund пишет:
> > Hi,
> > 
> > On 2025-06-02 21:20:33 +0300, Yura Sokolov wrote:
> >> But still problem of spin lock contention is here.
> > 
> > I still would like to see a reproducer for this.
> 
> For problem in sinvaladt.c we have no synthetic reproducer. But version
> with changed maxMsgNum to atomic solved customer's issue.

TBH, I don't see a point in continuing with this thread without something that
others can test.  I rather doubt that the right fix here is to just change the
lock model over, but without a repro I can't evaluate that.

Greetings,

Andres Freund





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

* Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-03 21:04             ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-06-16 14:28               ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-16 14:41                 ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
@ 2025-06-25 13:41                   ` Sergey Shinderuk <[email protected]>
  2025-07-16 14:58                     ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Sergey Shinderuk @ 2025-06-25 13:41 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; Yura Sokolov <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

On 16.06.2025 17:41, Andres Freund wrote:
> TBH, I don't see a point in continuing with this thread without something that
> others can test.  I rather doubt that the right fix here is to just change the
> lock model over, but without a repro I can't evaluate that.


Hello,

I think I can reproduce the issue with pgbench on a muti-core server. I 
start a regular select-only test with 64 clients, and while it's 
running, I start a plpgsql loop creating and dropping temporary tables 
from a single psql session. I observe ~25% drop in tps reported by 
pgbench until I cancel the query in psql.


$ pgbench -n -S -c64 -j64 -T300 -P1

progress: 10.0 s, 1249724.7 tps, lat 0.051 ms stddev 0.002, 0 failed
progress: 11.0 s, 1248289.0 tps, lat 0.051 ms stddev 0.002, 0 failed
progress: 12.0 s, 1246001.0 tps, lat 0.051 ms stddev 0.002, 0 failed
progress: 13.0 s, 1247832.5 tps, lat 0.051 ms stddev 0.002, 0 failed
progress: 14.0 s, 1248205.8 tps, lat 0.051 ms stddev 0.002, 0 failed
progress: 15.0 s, 1247737.3 tps, lat 0.051 ms stddev 0.002, 0 failed
progress: 16.0 s, 1219444.3 tps, lat 0.052 ms stddev 0.039, 0 failed
progress: 17.0 s, 893943.4 tps, lat 0.071 ms stddev 0.159, 0 failed
progress: 18.0 s, 927861.3 tps, lat 0.069 ms stddev 0.150, 0 failed
progress: 19.0 s, 886317.1 tps, lat 0.072 ms stddev 0.163, 0 failed
progress: 20.0 s, 877200.1 tps, lat 0.073 ms stddev 0.164, 0 failed
progress: 21.0 s, 875424.4 tps, lat 0.073 ms stddev 0.163, 0 failed
progress: 22.0 s, 877693.0 tps, lat 0.073 ms stddev 0.165, 0 failed
progress: 23.0 s, 897202.8 tps, lat 0.071 ms stddev 0.158, 0 failed
progress: 24.0 s, 917853.4 tps, lat 0.070 ms stddev 0.153, 0 failed
progress: 25.0 s, 907865.1 tps, lat 0.070 ms stddev 0.154, 0 failed

Here I started the following loop in psql around 17s and tps dropped by 
~25%:

do $$
begin
   for i in 1..1000000 loop
     create temp table tt1 (a bigserial primary key, b text);
     drop table tt1;
     commit;
   end loop;
end;
$$;

Now, if I simply remove the spinlock in SIGetDataEntries, I see a drop 
of just ~6% under concurrent DDL. I think this strongly suggests that 
the spinlock is the bottleneck.

Before that, I tried removing `if (!hasMessages) return` optimization in 
SIGetDataEntries to stress the spinlock and observed ~35% drop in tps of 
select-only with an empty sinval queue (no DDL running in background). 
Then I also removed the spinlock in SIGetDataEntries, and the loss was 
just ~4%, which may be noise. I think this also suggests that the 
spinlock could be the bottleneck.

I'm running this on a 2 socket AMD EPYC 9654 96-Core server with 
postgres and pgbench bound to distinct CPUs. PGDATA is placed on tmpfs. 
postgres is running with the default settings. pgbench tables are of 
scale 1. pgbench is connecting via loopback/127.0.0.1.

Does this sound convincing?

Best regards,

-- 
Sergey Shinderuk		https://postgrespro.com/






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

* Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-03 21:04             ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-06-16 14:28               ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-16 14:41                 ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-06-25 13:41                   ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Sergey Shinderuk <[email protected]>
@ 2025-07-16 14:58                     ` Andres Freund <[email protected]>
  2025-07-16 15:27                       ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Andres Freund @ 2025-07-16 14:58 UTC (permalink / raw)
  To: Sergey Shinderuk <[email protected]>; +Cc: Yura Sokolov <[email protected]>; Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Hi,

On 2025-06-25 16:41:46 +0300, Sergey Shinderuk wrote:
> On 16.06.2025 17:41, Andres Freund wrote:
> > TBH, I don't see a point in continuing with this thread without something that
> > others can test.  I rather doubt that the right fix here is to just change the
> > lock model over, but without a repro I can't evaluate that.

> I think I can reproduce the issue with pgbench on a muti-core server. I
> start a regular select-only test with 64 clients, and while it's running, I
> start a plpgsql loop creating and dropping temporary tables from a single
> psql session. I observe ~25% drop in tps reported by pgbench until I cancel
> the query in psql.

> 
> $ pgbench -n -S -c64 -j64 -T300 -P1
> 
> progress: 10.0 s, 1249724.7 tps, lat 0.051 ms stddev 0.002, 0 failed
> progress: 11.0 s, 1248289.0 tps, lat 0.051 ms stddev 0.002, 0 failed
> progress: 12.0 s, 1246001.0 tps, lat 0.051 ms stddev 0.002, 0 failed
> progress: 13.0 s, 1247832.5 tps, lat 0.051 ms stddev 0.002, 0 failed
> progress: 14.0 s, 1248205.8 tps, lat 0.051 ms stddev 0.002, 0 failed
> progress: 15.0 s, 1247737.3 tps, lat 0.051 ms stddev 0.002, 0 failed
> progress: 16.0 s, 1219444.3 tps, lat 0.052 ms stddev 0.039, 0 failed
> progress: 17.0 s, 893943.4 tps, lat 0.071 ms stddev 0.159, 0 failed
> progress: 18.0 s, 927861.3 tps, lat 0.069 ms stddev 0.150, 0 failed
> progress: 19.0 s, 886317.1 tps, lat 0.072 ms stddev 0.163, 0 failed
> progress: 20.0 s, 877200.1 tps, lat 0.073 ms stddev 0.164, 0 failed
> progress: 21.0 s, 875424.4 tps, lat 0.073 ms stddev 0.163, 0 failed
> progress: 22.0 s, 877693.0 tps, lat 0.073 ms stddev 0.165, 0 failed
> progress: 23.0 s, 897202.8 tps, lat 0.071 ms stddev 0.158, 0 failed
> progress: 24.0 s, 917853.4 tps, lat 0.070 ms stddev 0.153, 0 failed
> progress: 25.0 s, 907865.1 tps, lat 0.070 ms stddev 0.154, 0 failed
> 
> Here I started the following loop in psql around 17s and tps dropped by
> ~25%:
> 
> do $$
> begin
>   for i in 1..1000000 loop
>     create temp table tt1 (a bigserial primary key, b text);
>     drop table tt1;
>     commit;
>   end loop;
> end;
> $$;

Not a particularly interesting use-case, but still good to have.



> Now, if I simply remove the spinlock in SIGetDataEntries, I see a drop of
> just ~6% under concurrent DDL. I think this strongly suggests that the
> spinlock is the bottleneck.

This can be trivially optimized by just using atomics in a slightly more
heavyweight way than Yura's patch upthread - my criticisim in [1] was purely
that the reduced barriers make the patch incorrect.  I don't understand why
Yura didn't just move to full memory barriers, instead choosing to write a RW
optimized spinlock.

[1] https://postgr.es/m/5ccgykypol3azijw2chqnpg3rhuwjtwsmbfs3pgcqm7fu6laus%40wppo6zcfszay

Greetings,

Andres Freund





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

* Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-03 21:04             ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-06-16 14:28               ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-16 14:41                 ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-06-25 13:41                   ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Sergey Shinderuk <[email protected]>
  2025-07-16 14:58                     ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
@ 2025-07-16 15:27                       ` Yura Sokolov <[email protected]>
  2025-07-16 15:54                         ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  0 siblings, 1 reply; 84+ messages in thread

From: Yura Sokolov @ 2025-07-16 15:27 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; Sergey Shinderuk <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

16.07.2025 17:58, Andres Freund пишет:
> Hi,
> 
> On 2025-06-25 16:41:46 +0300, Sergey Shinderuk wrote:
>> On 16.06.2025 17:41, Andres Freund wrote:
>>> TBH, I don't see a point in continuing with this thread without something that
>>> others can test.  I rather doubt that the right fix here is to just change the
>>> lock model over, but without a repro I can't evaluate that.
> 
>> I think I can reproduce the issue with pgbench on a muti-core server. I
>> start a regular select-only test with 64 clients, and while it's running, I
>> start a plpgsql loop creating and dropping temporary tables from a single
>> psql session. I observe ~25% drop in tps reported by pgbench until I cancel
>> the query in psql.
> 
>>
>> $ pgbench -n -S -c64 -j64 -T300 -P1
>>
>> progress: 10.0 s, 1249724.7 tps, lat 0.051 ms stddev 0.002, 0 failed
>> progress: 11.0 s, 1248289.0 tps, lat 0.051 ms stddev 0.002, 0 failed
>> progress: 12.0 s, 1246001.0 tps, lat 0.051 ms stddev 0.002, 0 failed
>> progress: 13.0 s, 1247832.5 tps, lat 0.051 ms stddev 0.002, 0 failed
>> progress: 14.0 s, 1248205.8 tps, lat 0.051 ms stddev 0.002, 0 failed
>> progress: 15.0 s, 1247737.3 tps, lat 0.051 ms stddev 0.002, 0 failed
>> progress: 16.0 s, 1219444.3 tps, lat 0.052 ms stddev 0.039, 0 failed
>> progress: 17.0 s, 893943.4 tps, lat 0.071 ms stddev 0.159, 0 failed
>> progress: 18.0 s, 927861.3 tps, lat 0.069 ms stddev 0.150, 0 failed
>> progress: 19.0 s, 886317.1 tps, lat 0.072 ms stddev 0.163, 0 failed
>> progress: 20.0 s, 877200.1 tps, lat 0.073 ms stddev 0.164, 0 failed
>> progress: 21.0 s, 875424.4 tps, lat 0.073 ms stddev 0.163, 0 failed
>> progress: 22.0 s, 877693.0 tps, lat 0.073 ms stddev 0.165, 0 failed
>> progress: 23.0 s, 897202.8 tps, lat 0.071 ms stddev 0.158, 0 failed
>> progress: 24.0 s, 917853.4 tps, lat 0.070 ms stddev 0.153, 0 failed
>> progress: 25.0 s, 907865.1 tps, lat 0.070 ms stddev 0.154, 0 failed
>>
>> Here I started the following loop in psql around 17s and tps dropped by
>> ~25%:
>>
>> do $$
>> begin
>>   for i in 1..1000000 loop
>>     create temp table tt1 (a bigserial primary key, b text);
>>     drop table tt1;
>>     commit;
>>   end loop;
>> end;
>> $$;
> 
> Not a particularly interesting use-case, but still good to have.
> 
> 
> 
>> Now, if I simply remove the spinlock in SIGetDataEntries, I see a drop of
>> just ~6% under concurrent DDL. I think this strongly suggests that the
>> spinlock is the bottleneck.
> 
> This can be trivially optimized by just using atomics in a slightly more
> heavyweight way than Yura's patch upthread - my criticisim in [1] was purely
> that the reduced barriers make the patch incorrect.  I don't understand why
> Yura didn't just move to full memory barriers, instead choosing to write a RW
> optimized spinlock.
>
> [1] https://postgr.es/m/5ccgykypol3azijw2chqnpg3rhuwjtwsmbfs3pgcqm7fu6laus%40wppo6zcfszay

Excuse me, but I did at [1]. Not all barriers were changed to full, but one
necessary at the place you'd pointed to.

But you just answered then [2]:
> I don't believe we have the whole story here.

[1]
https://www.postgresql.org/message-id/b798eb5e-35b7-40b5-a245-4170deab56f8%40postgrespro.ru
[2]
https://www.postgresql.org/message-id/4yfgfeu3f6f7fayl5h3kggtd5bkvm4gj3a3ryjs2qhlo6g74bt%40g3cu2u4pg...

I moved to RW optimized spinlock because I found second place where it
helps. And patch includes both those places.
I think it is better to have good reusable tool applied in two places
instead of two custom pieces of code. And RWOptSpin is almost as good as
direct use of atomic operations.

-- 
regards
Yura Sokolov aka funny-falcon





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

* Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum)
  2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
  2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
  2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
  2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-03 21:04             ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-06-16 14:28               ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
  2025-06-16 14:41                 ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-06-25 13:41                   ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Sergey Shinderuk <[email protected]>
  2025-07-16 14:58                     ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
  2025-07-16 15:27                       ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
@ 2025-07-16 15:54                         ` Andres Freund <[email protected]>
  0 siblings, 0 replies; 84+ messages in thread

From: Andres Freund @ 2025-07-16 15:54 UTC (permalink / raw)
  To: Yura Sokolov <[email protected]>; +Cc: Sergey Shinderuk <[email protected]>; Heikki Linnakangas <[email protected]>; [email protected] <[email protected]>

Hi,

On 2025-07-16 18:27:45 +0300, Yura Sokolov wrote:
> 16.07.2025 17:58, Andres Freund пишет:
> >> Now, if I simply remove the spinlock in SIGetDataEntries, I see a drop of
> >> just ~6% under concurrent DDL. I think this strongly suggests that the
> >> spinlock is the bottleneck.
> > 
> > This can be trivially optimized by just using atomics in a slightly more
> > heavyweight way than Yura's patch upthread - my criticisim in [1] was purely
> > that the reduced barriers make the patch incorrect.  I don't understand why
> > Yura didn't just move to full memory barriers, instead choosing to write a RW
> > optimized spinlock.
> >
> > [1] https://postgr.es/m/5ccgykypol3azijw2chqnpg3rhuwjtwsmbfs3pgcqm7fu6laus%40wppo6zcfszay
> 
> Excuse me, but I did at [1]. Not all barriers were changed to full, but one
> necessary at the place you'd pointed to.

Then you went on to focus on a completely different approach.


> But you just answered then [2]:
> > I don't believe we have the whole story here.

That was not in reply to the changed patch, but about the performance numbers
you relayed. We had no repro, and even with the repro that Sergey has now
delivered, we don't see similar levels of what you reported as contention.

I still think that workloads bottlenecked by SI consumption could be improved
a lot more by focusing on higher level improvements than on the spinlock.


> [1]
> https://www.postgresql.org/message-id/b798eb5e-35b7-40b5-a245-4170deab56f8%40postgrespro.ru
> [2]
> https://www.postgresql.org/message-id/4yfgfeu3f6f7fayl5h3kggtd5bkvm4gj3a3ryjs2qhlo6g74bt%40g3cu2u4pg...
> 
> I moved to RW optimized spinlock because I found second place where it
> helps. And patch includes both those places.
> I think it is better to have good reusable tool applied in two places
> instead of two custom pieces of code. And RWOptSpin is almost as good as
> direct use of atomic operations.

Shrug. I don't think it makes sense here, given that the spinlock literally
just protects a single variable and that the most straightforward conversion
to atomics is like a 10 line change.

If somebody else feels different, they can pursue merging RWOptSpin, but I
won't. I think it's going 100% in the wrong direction.

Greetings,

Andres Freund





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


end of thread, other threads:[~2025-07-16 15:54 UTC | newest]

Thread overview: 84+ 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]>
2025-02-03 12:05 sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
2025-02-03 16:49 ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Heikki Linnakangas <[email protected]>
2025-02-04 10:36   ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
2025-03-21 11:35     ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
2025-03-21 16:33       ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
2025-03-24 10:41         ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
2025-03-24 13:08           ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Andres Freund <[email protected]>
2025-03-25 10:52             ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
2025-03-25 11:45               ` Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum Yura Sokolov <[email protected]>
2025-05-05 07:30         ` SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
2025-06-02 18:20           ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
2025-06-03 21:04             ` Re: SpinLockAcquire and SpinLockRelease is broken on ARM/ARM64? (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
2025-06-16 14:28               ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
2025-06-16 14:41                 ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
2025-06-25 13:41                   ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Sergey Shinderuk <[email protected]>
2025-07-16 14:58                     ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[email protected]>
2025-07-16 15:27                       ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Yura Sokolov <[email protected]>
2025-07-16 15:54                         ` Re: Read-Write optimistic lock (Re: sinvaladt.c: remove msgnumLock, use atomic operations on maxMsgNum) Andres Freund <[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