v11-0001-nbtree-merge-array-scankeys-s-arrays-more-effici.patch-b

application/octet-stream

Filename: v11-0001-nbtree-merge-array-scankeys-s-arrays-more-effici.patch-b
Type: application/octet-stream
Part: 0
Message: Re: Optimizing nbtree ScalarArrayOp execution, allowing multi-column ordered scans, skip scan
From eef1a00ebc208b8d6c6d02eb37325cd7602fa707 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Tue, 16 Jan 2024 18:34:09 +0100
Subject: [PATCH v11] nbtree: merge array scankeys's arrays more efficiently

Instead of n * log(m), we use mergejoin to merge the arrays in
O(n + m) time. We further use exponential search to improve
mergejoin's O(n+m) complexity to O(log(n | m)) in cases where one
array's data range is completely disjunct from the other.

While technically this last case could be further improved to O(1),
it'd be only a marginal speedup when compared to this one's.
---
 src/backend/access/nbtree/nbtutils.c | 207 ++++++++++++++++++++++++++-
 1 file changed, 205 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 78d16d3330..7013bf7315 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -48,6 +48,9 @@ static int	_bt_sort_array_elements(IndexScanDesc scan, ScanKey skey,
 static int	_bt_merge_arrays(IndexScanDesc scan, ScanKey skey, bool reverse,
 							 Datum *elems_orig, int nelems_orig,
 							 Datum *elems_next, int nelems_next);
+static int _bt_merge_arrays_search_next(Datum *elems, int nelems, int start_index,
+										Datum *key, BTSortArrayContext *cxt,
+										int *compare_result);
 static int	_bt_compare_array_elements(const void *a, const void *b, void *arg);
 static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc,
 										   Datum tupdatum, bool tupnull,
@@ -644,8 +647,9 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, bool reverse,
 {
 	BTScanOpaque so = (BTScanOpaque) scan->opaque;
 	BTSortArrayContext cxt;
-	Datum	   *merged = palloc(sizeof(Datum) * Min(nelems_orig, nelems_next));
+	Datum	   *merged PG_USED_FOR_ASSERTS_ONLY;
 	int			merged_nelems = 0;
+	int			merged_nelems_check PG_USED_FOR_ASSERTS_ONLY = 0;
 
 	/*
 	 * Incrementally copy the original array into a temp buffer, skipping over
@@ -654,25 +658,224 @@ _bt_merge_arrays(IndexScanDesc scan, ScanKey skey, bool reverse,
 	cxt.orderproc = &so->orderProcs[skey->sk_attno - 1];
 	cxt.collation = skey->sk_collation;
 	cxt.reverse = reverse;
+
+	/*
+	 * When assertions are enabled, use binary searches to create an array of
+	 * matches that we'll use to validate our merge join + exponential search
+	 * algorithm below.
+	 *
+	 * Note that this scratch space is only used in assert-enabled builds; we
+	 * write directly to elems_orig when we don't have assertions enabled,
+	 * saving one palloc/pfree.
+	 */
+#ifdef USE_ASSERT_CHECKING
+	merged = palloc(sizeof(Datum) * Min(nelems_orig, nelems_next));
+
 	for (int i = 0; i < nelems_orig; i++)
 	{
 		Datum	   *elem = elems_orig + i;
 
 		if (bsearch_arg(elem, elems_next, nelems_next, sizeof(Datum),
 						_bt_compare_array_elements, &cxt))
-			merged[merged_nelems++] = *elem;
+			merged[merged_nelems_check++] = *elem;
+	}
+#endif
+
+	for (int i = 0, j = 0; i < nelems_orig && j < nelems_next;)
+	{
+		Datum	   *orig = &elems_orig[i];
+		Datum	   *next = &elems_next[j];
+		int			res;
+
+		res = _bt_compare_array_elements(orig, next, &cxt);
+
+		/*
+		 * Ratchet each array forward until we find a match.
+		 */
+		do
+		{
+			if (res < 0)
+			{
+				int prev_i = i;
+				/*
+				 * Find the next element in elems_orig that is >= next,
+				 * storing the compare result in &res
+				 */
+				i = _bt_merge_arrays_search_next(elems_orig, nelems_orig, i,
+												 next, &cxt, &res);
+				Assert(i > prev_i);
+
+				/*
+				 * i is now either out of bounds, or has progressed to the
+				 * first offset that is >= next.
+				 */
+				if (i != nelems_orig)
+				{
+					orig = &elems_orig[i];
+					Assert(_bt_compare_array_elements(orig, next, &cxt) == res);
+					Assert(res >= 0);
+				}
+			}
+			else if (res > 0)
+			{
+				int prev_j = j;
+				/*
+				 * Find the next element in elems_next that is >= next,
+				 * storing the compare result in &res
+				 *
+				 * Note that this does compare(array_elem, key), so the
+				 * compare result in &res must be reversed before use in this
+				 * case.
+				 */
+				j = _bt_merge_arrays_search_next(elems_next, nelems_next, j,
+												 orig, &cxt, &res);
+				res = -res;
+				Assert(j > prev_j);
+
+				/*
+				 * j is now either out of bounds, or has progressed to the
+				 * first offset that is >= orig.
+				 */
+				if (j != nelems_next)
+				{
+					next = &elems_next[j];
+					Assert(_bt_compare_array_elements(orig, next, &cxt) == res);
+					Assert(res <= 0);
+				}
+			}
+		} while (res != 0 && i < nelems_orig && j < nelems_next);
+
+		/*
+		 * We are either at the end of one of the input arrays, or both of
+		 * the current array indexes are equal in this array entry.
+		 */
+		if (res != 0)
+		{
+			Assert(i == nelems_orig || j == nelems_next);
+		}
+		else /* res == 0 */
+		{
+#ifdef USE_ASSERT_CHECKING
+			/*
+			 * Make sure that the current index in elems_orig is not smaller
+			 * than the number of merged elements: if that were the case, we'd
+			 * have double-counted at least one element, which would break
+			 * the assumption we use in non-assertion builds to directly write
+			 * to elems_orig.
+			 */
+			Assert(merged_nelems <= i);
+			Assert(_bt_compare_array_elements(&merged[merged_nelems++], orig, &cxt) == 0);
+#else
+			/* Move the element to the merged section, if needed */
+			if (merged_nelems != i)
+				elems_orig[merged_nelems++] = *orig;
+#endif
+			i++;
+			j++;
+		}
 	}
 
+	Assert(merged_nelems == merged_nelems_check);
+
+#ifdef USE_ASSERT_CHECKING
 	/*
 	 * Overwrite the original array with temp buffer so that we're only left
 	 * with intersecting array elements
 	 */
 	memcpy(elems_orig, merged, merged_nelems * sizeof(Datum));
 	pfree(merged);
+#endif
 
 	return merged_nelems;
 }
 
+/*
+ * Find the next element in the array that is >= key, using cxt with
+ * _bt_compare_array_elements as sort operator.
+ * 
+ * Will always return a value > start_index.
+ *
+ * Takes O(1) if the next element is a neighbour, up to worst case
+ * O(log(n)) for n remaining entries.
+ *
+ * The function assumes that the input array is sorted.
+ */
+static int
+_bt_merge_arrays_search_next(Datum *elems, int nelems, int start_index,
+							 Datum *key, BTSortArrayContext *cxt,
+							 int *compare_result)
+{
+	int			step = 1;
+	int			min = start_index + 1,
+				max = start_index,
+				compare,
+				max_compare = -1;
+
+	/*
+	 * Exponential search forward to find the first element
+	 * 
+	 * We use exponential search forward to make sure we only need to do one
+	 * compare in many cases.
+	 * 
+	 */
+	for(;;)
+	{
+		max += step;
+
+		if (max >= nelems)
+		{
+			max = nelems;
+			compare = -1;
+			break;
+		}
+
+		compare = _bt_compare_array_elements(&elems[max], key, cxt);
+
+		step = step * 2;
+
+		if (compare < 0)
+			min = max + 1;
+		else
+			break;
+	}
+
+	max_compare = compare;
+
+	/* if we happened to land on an equal tuple we return early */
+	if (compare == 0)
+	{
+		*compare_result = compare;
+		return max;
+	}
+
+	/* Now do a binary search to get to the boundary position */
+	while (max > min)
+	{
+		int mid = min + ((max - min) / 2);
+
+		compare = _bt_compare_array_elements(&elems[mid], key, cxt);
+
+		if (compare < 0)
+		{
+			min = mid + 1;
+		}
+		else if (compare > 0)
+		{
+			max = mid;
+			max_compare = compare;
+		}
+		else if (compare == 0)
+		{
+			max = mid;
+			max_compare = compare;
+			break;
+		}
+	}
+
+	*compare_result = max_compare;
+	return max;
+}
+
 /*
  * qsort_arg comparator for sorting array elements
  */
-- 
2.40.1