Allow Incremental Sorts on GiST and SP-GiST indexes
authorDavid Rowley <[email protected]>
Tue, 4 Jul 2023 11:08:52 +0000 (23:08 +1200)
committerDavid Rowley <[email protected]>
Tue, 4 Jul 2023 11:08:52 +0000 (23:08 +1200)
Previously an "amcanorderbyop" index would only be used when the index
could provide sorted results which satisfied all query_pathkeys.  Here
we relax this so that we also allow these indexes to be considered by the
planner when they only provide partially sorted results.  This allows the
planner to later consider making use of an Incremental Sort to satisfy the
remaining pathkeys.  This change is particularly useful for KNN-type
queries which contain a LIMIT clause and an additional ORDER BY clause for
a non-indexed column.

Author: Miroslav Bendik
Reviewed-by: Richard Guo, David Rowley
Discussion: https://postgr.es/m/CAPoEpV0QYDtzjwamwWUBqyWpaCVbJV2d6qOD7Uy09bWn47PJtw%40mail.gmail.com

src/backend/optimizer/path/indxpath.c
src/test/regress/expected/incremental_sort.out
src/test/regress/sql/incremental_sort.sql

index 0065c8992bd5efdcaf4acd34f164e9f2a650bdbc..6a93d767a5c47c012f3dc2540b6fc5c9a9a9b29d 100644 (file)
@@ -974,14 +974,20 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
    }
    else if (index->amcanorderbyop && pathkeys_possibly_useful)
    {
-       /* see if we can generate ordering operators for query_pathkeys */
+       /*
+        * See if we can generate ordering operators for query_pathkeys or at
+        * least some prefix thereof.  Matching to just a prefix of the
+        * query_pathkeys will allow an incremental sort to be considered on
+        * the index's partially sorted results.
+        */
        match_pathkeys_to_index(index, root->query_pathkeys,
                                &orderbyclauses,
                                &orderbyclausecols);
-       if (orderbyclauses)
+       if (list_length(root->query_pathkeys) == list_length(orderbyclauses))
            useful_pathkeys = root->query_pathkeys;
        else
-           useful_pathkeys = NIL;
+           useful_pathkeys = list_copy_head(root->query_pathkeys,
+                                            list_length(orderbyclauses));
    }
    else
    {
@@ -3051,24 +3057,24 @@ expand_indexqual_rowcompare(PlannerInfo *root,
 
 /*
  * match_pathkeys_to_index
- *     Test whether an index can produce output ordered according to the
- *     given pathkeys using "ordering operators".
- *
- * If it can, return a list of suitable ORDER BY expressions, each of the form
- * "indexedcol operator pseudoconstant", along with an integer list of the
- * index column numbers (zero based) that each clause would be used with.
- * NIL lists are returned if the ordering is not achievable this way.
- *
- * On success, the result list is ordered by pathkeys, and in fact is
- * one-to-one with the requested pathkeys.
+ *     For the given 'index' and 'pathkeys', output a list of suitable ORDER
+ *     BY expressions, each of the form "indexedcol operator pseudoconstant",
+ *     along with an integer list of the index column numbers (zero based)
+ *     that each clause would be used with.
+ *
+ * This attempts to find an ORDER BY and index column number for all items in
+ * the pathkey list, however, if we're unable to match any given pathkey to an
+ * index column, we return just the ones matched by the function so far.  This
+ * allows callers who are interested in partial matches to get them.  Callers
+ * can determine a partial match vs a full match by checking the outputted
+ * list lengths.  A full match will have one item in the output lists for each
+ * item in the given 'pathkeys' list.
  */
 static void
 match_pathkeys_to_index(IndexOptInfo *index, List *pathkeys,
                        List **orderby_clauses_p,
                        List **clause_columns_p)
 {
-   List       *orderby_clauses = NIL;
-   List       *clause_columns = NIL;
    ListCell   *lc1;
 
    *orderby_clauses_p = NIL;   /* set default results */
@@ -3084,10 +3090,6 @@ match_pathkeys_to_index(IndexOptInfo *index, List *pathkeys,
        bool        found = false;
        ListCell   *lc2;
 
-       /*
-        * Note: for any failure to match, we just return NIL immediately.
-        * There is no value in matching just some of the pathkeys.
-        */
 
        /* Pathkey must request default sort order for the target opfamily */
        if (pathkey->pk_strategy != BTLessStrategyNumber ||
@@ -3133,8 +3135,8 @@ match_pathkeys_to_index(IndexOptInfo *index, List *pathkeys,
                                                   pathkey->pk_opfamily);
                if (expr)
                {
-                   orderby_clauses = lappend(orderby_clauses, expr);
-                   clause_columns = lappend_int(clause_columns, indexcol);
+                   *orderby_clauses_p = lappend(*orderby_clauses_p, expr);
+                   *clause_columns_p = lappend_int(*clause_columns_p, indexcol);
                    found = true;
                    break;
                }
@@ -3144,12 +3146,13 @@ match_pathkeys_to_index(IndexOptInfo *index, List *pathkeys,
                break;
        }
 
-       if (!found)             /* fail if no match for this pathkey */
+       /*
+        * Return the matches found so far when this pathkey couldn't be
+        * matched to the index.
+        */
+       if (!found)
            return;
    }
-
-   *orderby_clauses_p = orderby_clauses;   /* success! */
-   *clause_columns_p = clause_columns;
 }
 
 /*
index 0c3433f8e58e7b89bee72cb566a6d0a2e5c541d5..7fdb685313dbeee1a5f1554cc413b57af630085c 100644 (file)
@@ -1660,3 +1660,36 @@ order by 1, 2;
                ->  Function Scan on generate_series
 (7 rows)
 
+reset enable_hashagg;
+reset enable_seqscan;
+reset enable_incremental_sort;
+reset parallel_tuple_cost;
+reset parallel_setup_cost;
+reset min_parallel_table_scan_size;
+reset min_parallel_index_scan_size;
+-- Ensure incremental sorts work for amcanorderbyop type indexes
+create table point_table (a point, b int);
+create index point_table_a_idx on point_table using gist(a);
+-- Ensure we get an incremental sort plan for both of the following queries
+explain (costs off) select a, b, a <-> point(5, 5) dist from point_table order by dist, b limit 1;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Limit
+   ->  Incremental Sort
+         Sort Key: ((a <-> '(5,5)'::point)), b
+         Presorted Key: ((a <-> '(5,5)'::point))
+         ->  Index Scan using point_table_a_idx on point_table
+               Order By: (a <-> '(5,5)'::point)
+(6 rows)
+
+explain (costs off) select a, b, a <-> point(5, 5) dist from point_table order by dist, b desc limit 1;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Limit
+   ->  Incremental Sort
+         Sort Key: ((a <-> '(5,5)'::point)), b DESC
+         Presorted Key: ((a <-> '(5,5)'::point))
+         ->  Index Scan using point_table_a_idx on point_table
+               Order By: (a <-> '(5,5)'::point)
+(6 rows)
+
index 071f8a5268e98d93c86ad4fb56916aefc36fc867..ab471bdfffc12b3217608d8ae6a3f141626b29b3 100644 (file)
@@ -276,3 +276,19 @@ from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub;
 explain (costs off) select sub.unique1, stringu1 || random()::text
 from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub
 order by 1, 2;
+
+reset enable_hashagg;
+reset enable_seqscan;
+reset enable_incremental_sort;
+reset parallel_tuple_cost;
+reset parallel_setup_cost;
+reset min_parallel_table_scan_size;
+reset min_parallel_index_scan_size;
+
+-- Ensure incremental sorts work for amcanorderbyop type indexes
+create table point_table (a point, b int);
+create index point_table_a_idx on point_table using gist(a);
+
+-- Ensure we get an incremental sort plan for both of the following queries
+explain (costs off) select a, b, a <-> point(5, 5) dist from point_table order by dist, b limit 1;
+explain (costs off) select a, b, a <-> point(5, 5) dist from point_table order by dist, b desc limit 1;