From 1a7872332b202affd916374f3bae565ff8e7ec7d Mon Sep 17 00:00:00 2001 From: Alexandre Felipe Date: Fri, 10 Jul 2026 11:45:13 +0100 Subject: [PATCH-v10 5/8] SLOPE: Planner support Update the planner to take advantage of order of an expression that can be inferred from the order of a subexpression. If f(x) is a monotonic function and x is known to be ordered, we can infeer the order of f(x) from the order of x. The analysis is performed in two stages 1. During plan creation, for every pathkey expression computes the source of variation, defined as the innermost subexpression that causes all variation in the expression. if the source of variaiton is not the full expression, and depends on a single table, a slope_info entry is created. 2. on build_index_pathkeys, in addition to the usual pathkeys create pathkeys for slope_info entries where the index key is the source of variation. Changes: - Add SlopeInfo struct to cache monotonicity information per pathkey - Add precompute_slope_cache() called during plan construction - Add get_variation_source() to find the innermost varying expression - Add get_expr_slope_wrt() to verify monotonicity by calling prosupport - Modify build_index_pathkeys() to check for monotonicity on index keys - Handle both increasing and decreasing monotonic functions, with reversed pathkey emission for decreasing functions so that backward index scans are correctly selected Also adds the prosupport function entries to pg_proc.dat - arg0_asc_slope_support (OID 9954) - arg0_desc_slope_support (OID 9955) - arg1_asc_slope_support (OID 9956) - diff_slope_support (OID 9957) - addition_slope_support (OID 9958) - multiply_slope_support (OID 9959) - divide_slope_support (OID 9960) --- src/backend/optimizer/path/pathkeys.c | 412 +++++++++++++++ src/backend/optimizer/plan/planner.c | 3 + src/backend/utils/misc/guc_parameters.dat | 8 + src/include/nodes/pathnodes.h | 10 + src/include/nodes/plannodes.h | 7 + src/include/optimizer/paths.h | 2 + src/test/regress/expected/slope.out | 600 +++++++++++++++++++++- src/test/regress/sql/slope.sql | 386 +++++++++++++- 8 files changed, 1420 insertions(+), 8 deletions(-) diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index 1c89d9db997..b1bc68070a1 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -18,25 +18,37 @@ #include "postgres.h" #include "access/stratnum.h" +#include "access/transam.h" #include "catalog/pg_opfamily.h" +#include "catalog/pg_type.h" +#include "fmgr.h" #include "nodes/nodeFuncs.h" +#include "nodes/supportnodes.h" #include "optimizer/cost.h" #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" #include "optimizer/paths.h" +#include "parser/parse_oper.h" #include "partitioning/partbounds.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" +#include "utils/typcache.h" /* Consider reordering of GROUP BY keys? */ bool enable_group_by_reordering = true; +/* Perform slope analysis to identify indirectly sorted data */ +bool enable_slope = true; static bool pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys); static bool matches_boolean_partition_clause(RestrictInfo *rinfo, RelOptInfo *partrel, int partkeycol); static Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle); static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey); +static MonotonicFunction get_expr_slope_wrt(Expr *expr, Expr *target); +static PathKey *slope_emit_pathkey(PlannerInfo *root, PathKey *pk, + Expr *indexkey, bool reverse_sort, + bool nulls_first); /**************************************************************************** @@ -771,6 +783,366 @@ get_cheapest_parallel_safe_total_inner(List *paths) return NULL; } +/* + * get_variation_source + * Find the source of variation in an expression. + * + * Descends through function calls to find the innermost non-constant + * expression that determines the variation of the whole expression. + * For f(x) returns x. For f(g(x)) returns x. For f(x, y) returns f(x, y). + * For a plain Var, returns the Var itself. + * + * This is a cheap extraction that doesn't check monotonicity - that's + * deferred until we find an index column matching the variation source. + * Also extracts the relid if all Vars are from the same table. + */ +static void +get_variation_source(Expr *expr, Expr **inner_out, Index *relid_out) +{ + *inner_out = NULL; + *relid_out = 0; + + for (;;) + { + List *args; + Expr *non_const_arg = NULL; + int non_const_count = 0; + ListCell *lc; + + /* Skip RelabelType (no-op coercion) */ + if (IsA(expr, RelabelType)) + { + expr = (Expr *) ((RelabelType *) expr)->arg; + continue; + } + + /* Handle FuncExpr - skip through casts */ + if (IsA(expr, FuncExpr)) + { + FuncExpr *fexpr = (FuncExpr *) expr; + + if ((fexpr->funcformat == COERCE_IMPLICIT_CAST || + fexpr->funcformat == COERCE_EXPLICIT_CAST) && + list_length(fexpr->args) == 1) + { + expr = (Expr *) linitial(fexpr->args); + continue; + } + args = fexpr->args; + } + else if (IsA(expr, OpExpr)) + { + args = ((OpExpr *) expr)->args; + } + else if (IsA(expr, Var)) + { + /* Reached a Var - this is our inner expression */ + *inner_out = expr; + *relid_out = ((Var *) expr)->varno; + return; + } + else + { + /* Unsupported node type */ + return; + } + + /* Find non-constant arguments */ + foreach(lc, args) + { + Expr *arg = (Expr *) lfirst(lc); + + if (!IsA(arg, Const)) + { + non_const_count++; + if (non_const_count > 1) + { + /* Multivariate - return this expression as inner */ + *inner_out = expr; + *relid_out = 0; /* unknown, will use equal() */ + return; + } + non_const_arg = arg; + } + } + + if (non_const_arg == NULL) + { + /* All constant - no inner expression */ + return; + } + + expr = non_const_arg; + } +} + + +/* + * Returns true if the expression's type may hold NaN values. + */ +static bool +expr_can_nan(Expr *target) +{ + + Oid expr_type; + + /* Unwrap RelabelType (no-op coercion) */ + while (target != NULL && IsA(target, RelabelType)) + target = (Expr *) ((RelabelType *) target)->arg; + + /* Extract type from the node */ + if (IsA(target, Var)) + expr_type = ((Var *) target)->vartype; + else if (IsA(target, FuncExpr)) + expr_type = ((FuncExpr *) target)->funcresulttype; + else if (IsA(target, OpExpr)) + expr_type = ((OpExpr *) target)->opresulttype; + else + return false; + + /* Get the base type for domains */ + if (expr_type >= FirstNormalObjectId) + expr_type = getBaseType(expr_type); + + /* Check if the type can hold NaN values */ + switch (expr_type) + { + case FLOAT4OID: + case FLOAT8OID: + case NUMERICOID: + return true; + default: + return false; + } +} + +/* + * get_expr_slope_wrt + * Determine the monotonicity slope of an expression with respect to + * a specific target subexpression. + * + * Returns the slope of 'expr' with respect to 'target' + * MONOTONICFUNC_INCREASING: monotonically increasing + * MONOTONICFUNC_DECREASING: monotonically decreasing + * MONOTONICFUNC_BOTH: independent of target (constant) + * MONOTONICFUNC_NONE: cannot determine monotonicity + */ +static MonotonicFunction +get_expr_slope_wrt(Expr *expr, Expr *target) +{ + MonotonicFunction slope = MONOTONICFUNC_INCREASING; + + for (;;) + { + Oid funcid; + List *args; + Oid prosupport; + SupportRequestMonotonic req; + ListCell *lc; + int i; + Expr *next_expr = NULL; + MonotonicFunction func_arg_slope = MONOTONICFUNC_INCREASING; + + /* Check if we've reached the target */ + if (equal(expr, target)) + { + if (slope == MONOTONICFUNC_INCREASING) + return slope; + + /* + * A decreasing function does not relocate NaN values, so index + * scan ordering (forward or backward) cannot match ORDER BY f(x) + * on float/numeric columns. + */ + if (expr_can_nan(target)) + { + if (slope == MONOTONICFUNC_DECREASING) + return MONOTONICFUNC_NONE; + else if (slope == MONOTONICFUNC_BOTH) + return MONOTONICFUNC_INCREASING; + } + return slope; + } + + /* Skip RelabelType (no-op coercion) */ + if (IsA(expr, RelabelType)) + { + expr = (Expr *) ((RelabelType *) expr)->arg; + continue; + } + + /* Handle FuncExpr - skip through casts */ + if (IsA(expr, FuncExpr)) + { + FuncExpr *fexpr = (FuncExpr *) expr; + + if ((fexpr->funcformat == COERCE_IMPLICIT_CAST || + fexpr->funcformat == COERCE_EXPLICIT_CAST) && + list_length(fexpr->args) == 1) + { + expr = (Expr *) linitial(fexpr->args); + continue; + } + funcid = fexpr->funcid; + args = fexpr->args; + } + else if (IsA(expr, OpExpr)) + { + OpExpr *opexpr = (OpExpr *) expr; + + set_opfuncid(opexpr); + funcid = opexpr->opfuncid; + args = opexpr->args; + } + else + { + /* Reached a leaf without finding target */ + return MONOTONICFUNC_NONE; + } + + /* Check for prosupport function */ + prosupport = get_func_support(funcid); + if (!OidIsValid(prosupport)) + return MONOTONICFUNC_NONE; + + /* Call prosupport to get slope pattern */ + req.type = T_SupportRequestMonotonic; + req.expr = (Node *) expr; + req.slopes = NULL; + req.nslopes = 0; + + if (DatumGetPointer(OidFunctionCall1(prosupport, PointerGetDatum(&req))) == NULL) + return MONOTONICFUNC_NONE; + + if (req.slopes == NULL || req.nslopes <= 0) + return MONOTONICFUNC_NONE; + + /* Find the single non-constant argument */ + i = 0; + foreach(lc, args) + { + Expr *arg = (Expr *) lfirst(lc); + + if (!IsA(arg, Const)) + { + if (next_expr != NULL) + { + /* Multivariate - check if this is the target */ + return equal(expr, target) ? slope : MONOTONICFUNC_NONE; + } + next_expr = arg; + if (likely(i < req.nslopes)) + { + if (req.slopes[i] == MONOTONICFUNC_DECREASING) + func_arg_slope = MONOTONICFUNC_DECREASING; + else if (req.slopes[i] != MONOTONICFUNC_INCREASING) + return MONOTONICFUNC_NONE; + } + else + return MONOTONICFUNC_NONE; + } + i++; + } + + if (next_expr == NULL) + return MONOTONICFUNC_NONE; /* all constant */ + + /* Compose slopes */ + if (func_arg_slope == MONOTONICFUNC_DECREASING) + { + slope = (slope == MONOTONICFUNC_INCREASING) ? + MONOTONICFUNC_DECREASING : MONOTONICFUNC_INCREASING; + } + + expr = next_expr; + } +} + +/* + * precompute_slope_pathkeys + * For each query pathkey, extract the source of variation and store + * it directly on the PathKey (pk_var, pk_varrelid). + * + * Called once after query_pathkeys is set. Pathkeys whose expression + * is a plain Var or that have no usable variation source are left with + * pk_var = NULL. Monotonicity (pk_slope) is computed lazily on first + * index match. + */ +void +precompute_slope_pathkeys(PlannerInfo *root) +{ + ListCell *lc; + + foreach(lc, root->query_pathkeys) + { + PathKey *pk = lfirst_node(PathKey, lc); + EquivalenceMember *em; + + pk->pk_var = NULL; + pk->pk_varrelid = 0; + pk->pk_slope = MONOTONICFUNC_UNSET; + + if (!enable_slope) + continue; + + if (pk->pk_eclass->ec_has_volatile || + pk->pk_eclass->ec_members == NIL) + continue; + + em = linitial(pk->pk_eclass->ec_members); + + if (IsA(em->em_expr, Var)) + continue; + + get_variation_source(em->em_expr, &pk->pk_var, &pk->pk_varrelid); + + /* Discard if no useful source or it equals the full expression */ + if (pk->pk_var == NULL || pk->pk_varrelid == 0 || + pk->pk_var == em->em_expr) + pk->pk_var = NULL; + } +} + +/* + * slope_emit_pathkey + * Return the canonical pathkey for what a forward index scan actually + * produces for an expression that is monotonic in the index column. + * + * The result reflects both the direction and null ordering that the + * forward scan generates. f(NULL) is NULL, so nulls appear at the + * position dictated by the index's null ordering. + * + * Returns NULL if pk_slope indicates no monotonicity. + * + * index function pathkey + * ASC ASC ASC + * ASC DESC DESC + * DESC ASC DESC + * DESC DESC ASC + */ +static PathKey * +slope_emit_pathkey(PlannerInfo *root, + PathKey *pk, + Expr *indexkey, + bool reverse_sort, + bool nulls_first) +{ + MonotonicFunction slope; + bool produces_desc; + + slope = (MonotonicFunction) pk->pk_slope; + if (slope == MONOTONICFUNC_NONE) + return NULL; + + produces_desc = (reverse_sort != (slope == MONOTONICFUNC_DECREASING)); + + return make_canonical_pathkey(root, + pk->pk_eclass, + pk->pk_opfamily, + produces_desc ? COMPARE_GT : COMPARE_LT, + nulls_first); +} + /**************************************************************************** * NEW PATHKEY FORMATION ****************************************************************************/ @@ -843,6 +1215,46 @@ build_index_pathkeys(PlannerInfo *root, index->rel->relids, false); + /* + * SLOPE: if the first unmatched query pathkey is a monotonic function + * of this index column, use that pathkey instead of the column's own + * pathkey so the index can satisfy the query ordering without a Sort. + */ + if (enable_slope && index->rel->reloptkind == RELOPT_BASEREL) + { + ListCell *lc2; + + foreach(lc2, root->query_pathkeys) + { + PathKey *qpk = lfirst_node(PathKey, lc2); + + if (pathkey_is_redundant(qpk, retval)) + continue; + + if (cpathkey && qpk->pk_eclass == cpathkey->pk_eclass) + break; + + if (qpk->pk_var != NULL && + !qpk->pk_eclass->ec_has_volatile && + qpk->pk_varrelid == index->rel->relid && + equal(qpk->pk_var, indexkey)) + { + + if (qpk->pk_slope == MONOTONICFUNC_UNSET) + { + EquivalenceMember *em; + + em = linitial(qpk->pk_eclass->ec_members); + qpk->pk_slope = get_expr_slope_wrt(em->em_expr, + qpk->pk_var); + } + cpathkey = slope_emit_pathkey(root, qpk, indexkey, + reverse_sort, nulls_first); + } + break; + } + } + if (cpathkey) { /* diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 3225185d16f..23330cdbc14 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -3911,6 +3911,9 @@ standard_qp_callback(PlannerInfo *root, void *extra) root->query_pathkeys = root->setop_pathkeys; else root->query_pathkeys = NIL; + + /* Annotate query pathkeys with variation sources for SLOPE */ + precompute_slope_pathkeys(root); } /* diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index d421cdbde76..2d218634d4b 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -1033,6 +1033,14 @@ boot_val => 'true', }, +{ name => 'enable_slope', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s slope analysis.', + long_desc => 'By employing slope analysis the planner can take advantage of an index order of a monotonic expression of an index key, eliminating unnecessary sort nodes.', + flags => 'GUC_EXPLAIN', + variable => 'enable_slope', + boot_val => 'true', +}, + { name => 'enable_sort', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', short_desc => 'Enables the planner\'s use of explicit sort steps.', flags => 'GUC_EXPLAIN', diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 27a2c6815b7..49ac9a66778 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1814,6 +1814,16 @@ typedef struct PathKey Oid pk_opfamily; /* index opfamily defining the ordering */ CompareType pk_cmptype; /* sort direction (ASC or DESC) */ bool pk_nulls_first; /* do NULLs come before normal values? */ + + /* + * SLOPE: innermost source of variation, filled by + * precompute_slope_pathkeys(). NULL if this pathkey is a plain Var or + * cannot benefit from SLOPE. pk_slope stores a MonotonicFunction value + * (from plannodes.h) as int to avoid a header dependency. + */ + Expr *pk_var pg_node_attr(read_write_ignore, equal_ignore); + Index pk_varrelid pg_node_attr(read_write_ignore, equal_ignore); + int pk_slope pg_node_attr(read_write_ignore, equal_ignore); } PathKey; /* diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index b880ce0f4be..9f17cc997f1 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -1835,6 +1835,11 @@ typedef struct PlanInvalItem * than the previous call. A monotonically decreasing function cannot yield a * higher value on subsequent calls, and a function which is both must return * the same value on each call. + * + * Used both for window function run conditions (SupportRequestWFuncMonotonic) + * and for per-argument monotonicity of scalar functions + * (SupportRequestMonotonic), where it enables the planner to use an index + * on 'x' to satisfy ORDER BY / GROUP BY on 'f(x)'. */ typedef enum MonotonicFunction { @@ -1842,6 +1847,8 @@ typedef enum MonotonicFunction MONOTONICFUNC_INCREASING = (1 << 0), MONOTONICFUNC_DECREASING = (1 << 1), MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING, + /* Sentinel for uninitialised values */ + MONOTONICFUNC_UNSET = (1 << 2), } MonotonicFunction; /* diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 4ad3e8eaa89..cafcbce7d56 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -27,6 +27,7 @@ extern PGDLLIMPORT double min_eager_agg_group_size; extern PGDLLIMPORT int min_parallel_table_scan_size; extern PGDLLIMPORT int min_parallel_index_scan_size; extern PGDLLIMPORT bool enable_group_by_reordering; +extern PGDLLIMPORT bool enable_slope; /* Hooks for plugins to get control in set_rel_pathlist() */ typedef void (*join_path_setup_hook_type) (PlannerInfo *root, @@ -235,6 +236,7 @@ extern Path *get_cheapest_fractional_path_for_pathkeys(List *paths, Relids required_outer, double fraction); extern Path *get_cheapest_parallel_safe_total_inner(List *paths); +extern void precompute_slope_pathkeys(PlannerInfo *root); extern List *build_index_pathkeys(PlannerInfo *root, IndexOptInfo *index); extern PathKey *make_reversed_pathkey(PlannerInfo *root, PathKey *pathkey); extern List *reverse_pathkeys(PlannerInfo *root, List *pathkeys); diff --git a/src/test/regress/expected/slope.out b/src/test/regress/expected/slope.out index a72195a9fec..c629bbec9ac 100644 --- a/src/test/regress/expected/slope.out +++ b/src/test/regress/expected/slope.out @@ -1,10 +1,25 @@ +-- Configure parameters to avoid competing plans. +-- Prevent seqscans or bitmapscans that could be combined +-- with a sort as an alternative to an index scan that +-- satisfies the query order. +SET enable_seqscan = off; +SET enable_bitmapscan = off; +-- Every query should rely on an index, and a scan without +-- a sort will always be cheaper than a scan with a sort. +SET enable_indexscan = on; +SET enable_indexonlyscan = on; +-- Disable hashagg that could provide an alterntative to a +-- GROUP BY that doesn't require the input to be sorted. +SET enable_hashagg = off; +-- +-- Check the ability to use index scans when the index +-- for different nulls order, on either nullable or non-nullable columns. +-- CREATE TABLE slope_t1 (a int, b int NOT NULL); INSERT INTO slope_t1 SELECT i, i FROM generate_series(1, 10) AS i; CREATE INDEX slope_t1_a_idx ON slope_t1 (a); CREATE INDEX slope_t1_b_idx ON slope_t1 (b); ANALYSE slope_t1; -SET enable_seqscan = off; -SET enable_bitmapscan = off; -- Column `a` can be scanned in two directions, but the -- nulls order of the query must match the nulls order of the index. EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 ASC NULLS FIRST; @@ -90,3 +105,584 @@ EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 DESC NULLS LAST; (2 rows) DROP TABLE slope_t1; +-- +-- SLOPE (Scalar function Leveraging Ordered Path Evaluation) +-- Test that monotonic functions can use indexes for ordering +-- +-- Create test table with various data types +CREATE TABLE slope_src ( + id serial PRIMARY KEY, + v_int2 int2, + v_int4 int4, + v_int8 int8, + v_float4 float4, + v_float8 float8, + v_numeric numeric, + ts timestamp, + tstz timestamptz +); +-- Insert some test data +INSERT INTO slope_src (v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz) +SELECT + (i % 100)::int2, + i, + i::int8, + i::float4, + i::float8, + i::numeric, + '2020-01-01'::timestamp + (i || ' hours')::interval, + '2020-01-01'::timestamptz + (i || ' hours')::interval +FROM generate_series(1, 1000) i; +-- Create indexes on the columns we'll test +CREATE INDEX slope_src_v_int4_idx ON slope_src (v_int4); +CREATE INDEX slope_src_v_int8_idx ON slope_src (v_int8); +CREATE INDEX slope_src_v_float8_idx ON slope_src (v_float8); +CREATE INDEX slope_src_v_numeric_idx ON slope_src (v_numeric); +CREATE INDEX slope_src_ts_idx ON slope_src (ts); +CREATE INDEX slope_src_tstz_idx ON slope_src (tstz); +-- Analyze to get good statistics +ANALYZE slope_src; +-- +-- Test that SLOPE is enabled and can be disabled +-- +SET enable_slope = off; +-- Basic: floor(float8) should sort even though the index order +-- produces the result in the correct order. +explain (costs off, verbose) +select floor(v_float8), count(*) from slope_src group by 1; + QUERY PLAN +------------------------------------------------------------------------------ + GroupAggregate + Output: (floor(v_float8)), count(*) + Group Key: (floor(slope_src.v_float8)) + -> Sort + Output: (floor(v_float8)) + Sort Key: (floor(slope_src.v_float8)) + -> Index Only Scan using slope_src_v_float8_idx on public.slope_src + Output: floor(v_float8) +(8 rows) + +RESET enable_slope; +SHOW enable_slope; + enable_slope +-------------- + on +(1 row) + +-- +-- Test GROUP BY with monotonic function +-- +-- Basic: floor(float8) should use index on v_float8 +explain (costs off, verbose) +select floor(v_float8), count(*) from slope_src group by 1; + QUERY PLAN +------------------------------------------------------------------------ + GroupAggregate + Output: (floor(v_float8)), count(*) + Group Key: floor(slope_src.v_float8) + -> Index Only Scan using slope_src_v_float8_idx on public.slope_src + Output: floor(v_float8) +(5 rows) + +-- ceil(float8) should use index on v_float8 +explain (costs off, verbose) +select ceil(v_float8), count(*) from slope_src group by 1; + QUERY PLAN +------------------------------------------------------------------------ + GroupAggregate + Output: (ceil(v_float8)), count(*) + Group Key: ceil(slope_src.v_float8) + -> Index Only Scan using slope_src_v_float8_idx on public.slope_src + Output: ceil(v_float8) +(5 rows) + +-- floor(numeric) should use index on v_numeric +explain (costs off, verbose) +select floor(v_numeric), count(*) from slope_src group by 1; + QUERY PLAN +------------------------------------------------------------------------- + GroupAggregate + Output: (floor(v_numeric)), count(*) + Group Key: floor(slope_src.v_numeric) + -> Index Only Scan using slope_src_v_numeric_idx on public.slope_src + Output: floor(v_numeric) +(5 rows) + +-- timestamp::date cast should use index on ts +explain (costs off, verbose) +select ts::date, count(*) from slope_src group by 1; + QUERY PLAN +------------------------------------------------------------------ + GroupAggregate + Output: ((ts)::date), count(*) + Group Key: (slope_src.ts)::date + -> Index Only Scan using slope_src_ts_idx on public.slope_src + Output: (ts)::date +(5 rows) + +-- date_trunc on timestamp should use index +explain (costs off, verbose) +select date_trunc('day', ts), count(*) from slope_src group by 1; + QUERY PLAN +------------------------------------------------------------------ + GroupAggregate + Output: (date_trunc('day'::text, ts)), count(*) + Group Key: date_trunc('day'::text, slope_src.ts) + -> Index Only Scan using slope_src_ts_idx on public.slope_src + Output: date_trunc('day'::text, ts) +(5 rows) + +-- date_trunc on timestamptz should use index +explain (costs off, verbose) +select date_trunc('day', tstz), count(*) from slope_src group by 1; + QUERY PLAN +-------------------------------------------------------------------- + GroupAggregate + Output: (date_trunc('day'::text, tstz)), count(*) + Group Key: date_trunc('day'::text, slope_src.tstz) + -> Index Only Scan using slope_src_tstz_idx on public.slope_src + Output: date_trunc('day'::text, tstz) +(5 rows) + +-- +-- Test arithmetic operations +-- +-- Addition: v_int4 + 10 is increasing in v_int4 +explain (costs off, verbose) +select v_int4 + 10, count(*) from slope_src group by 1; + QUERY PLAN +---------------------------------------------------------------------- + GroupAggregate + Output: ((v_int4 + 10)), count(*) + Group Key: (slope_src.v_int4 + 10) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (v_int4 + 10) +(5 rows) + +-- Subtraction: v_int4 - 10 is increasing in v_int4 +explain (costs off, verbose) +select v_int4 - 10, count(*) from slope_src group by 1; + QUERY PLAN +---------------------------------------------------------------------- + GroupAggregate + Output: ((v_int4 - 10)), count(*) + Group Key: (slope_src.v_int4 - 10) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (v_int4 - 10) +(5 rows) + +-- Multiplication by positive constant: v_int4 * 2 is increasing +explain (costs off, verbose) +select v_int4 * 2, count(*) from slope_src group by 1; + QUERY PLAN +---------------------------------------------------------------------- + GroupAggregate + Output: ((v_int4 * 2)), count(*) + Group Key: (slope_src.v_int4 * 2) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (v_int4 * 2) +(5 rows) + +-- Division by positive constant: v_int4 / 2 is increasing +explain (costs off, verbose) +select v_int4 / 2, count(*) from slope_src group by 1; + QUERY PLAN +---------------------------------------------------------------------- + GroupAggregate + Output: ((v_int4 / 2)), count(*) + Group Key: (slope_src.v_int4 / 2) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (v_int4 / 2) +(5 rows) + +-- +-- Test decreasing functions +-- These queries can't use the index order because group pathkeys +-- are always ASC NULLS LAST, while -int_v4 is DESC NULLS LAST. +-- Unary minus: -v_int4 is decreasing in v_int4 +explain (costs off, verbose) +select -v_int4, count(*) from slope_src group by 1; + QUERY PLAN +---------------------------------------------------------------------------- + GroupAggregate + Output: ((- v_int4)), count(*) + Group Key: ((- slope_src.v_int4)) + -> Sort + Output: ((- v_int4)) + Sort Key: ((- slope_src.v_int4)) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (- v_int4) +(8 rows) + +-- Subtraction from constant: 1000 - v_int4 is decreasing in v_int4 +explain (costs off, verbose) +select 1000 - v_int4, count(*) from slope_src group by 1; + QUERY PLAN +---------------------------------------------------------------------------- + GroupAggregate + Output: ((1000 - v_int4)), count(*) + Group Key: ((1000 - slope_src.v_int4)) + -> Sort + Output: ((1000 - v_int4)) + Sort Key: ((1000 - slope_src.v_int4)) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (1000 - v_int4) +(8 rows) + +-- Multiplication by negative constant: v_int4 * (-2) is decreasing +explain (costs off, verbose) +select v_int4 * (-2), count(*) from slope_src group by 1; + QUERY PLAN +---------------------------------------------------------------------------- + GroupAggregate + Output: ((v_int4 * '-2'::integer)), count(*) + Group Key: ((slope_src.v_int4 * '-2'::integer)) + -> Sort + Output: ((v_int4 * '-2'::integer)) + Sort Key: ((slope_src.v_int4 * '-2'::integer)) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (v_int4 * '-2'::integer) +(8 rows) + +-- Division by negative constant: v_int4 / (-2) is decreasing +explain (costs off, verbose) +select v_int4 / (-2), count(*) from slope_src group by 1; + QUERY PLAN +---------------------------------------------------------------------------- + GroupAggregate + Output: ((v_int4 / '-2'::integer)), count(*) + Group Key: ((slope_src.v_int4 / '-2'::integer)) + -> Sort + Output: ((v_int4 / '-2'::integer)) + Sort Key: ((slope_src.v_int4 / '-2'::integer)) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (v_int4 / '-2'::integer) +(8 rows) + +-- +-- Test ORDER BY with monotonic function +-- +-- ORDER BY floor(v_float8) should use index +explain (costs off, verbose) +select floor(v_float8), v_float8 from slope_src order by 1 limit 10; + QUERY PLAN +------------------------------------------------------------------------ + Limit + Output: (floor(v_float8)), v_float8 + -> Index Only Scan using slope_src_v_float8_idx on public.slope_src + Output: floor(v_float8), v_float8 +(4 rows) + +-- ORDER BY -v_int4 DESC requires sorting +-- the index order is v_int4 ASC and, implicitly, NULLS LAST +-- a forward scan gives -v_int4 DESC NULLS LAST +-- a backward scan gives -v_int4 ASC NULLS FIRST +-- the query order is DESC and, implicitly, NULLS FIRST +explain (costs off, verbose) +select -v_int4 from slope_src order by 1 desc limit 10; + QUERY PLAN +---------------------------------------------------------------------------- + Limit + Output: ((- v_int4)) + -> Sort + Output: ((- v_int4)) + Sort Key: ((- slope_src.v_int4)) DESC + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (- v_int4) +(7 rows) + +-- ORDER BY -v_int4 ASC requires sorting +-- a forward scan gives -v_int4 DESC NULLS LAST +-- a backward scan gives -v_int4 ASC NULLS FIRST +-- the query order is -v_int4 ASC NULLS LAST +explain (costs off, verbose) +select -v_int4 from slope_src order by 1 limit 10; + QUERY PLAN +---------------------------------------------------------------------------- + Limit + Output: ((- v_int4)) + -> Sort + Output: ((- v_int4)) + Sort Key: ((- slope_src.v_int4)) + -> Index Only Scan using slope_src_v_int4_idx on public.slope_src + Output: (- v_int4) +(7 rows) + +-- +-- Group and order +-- +explain (costs off, verbose) +select tstz::date, count(*) from slope_src group by 1 order by 1 limit 10; + QUERY PLAN +-------------------------------------------------------------------------- + Limit + Output: ((tstz)::date), (count(*)) + -> GroupAggregate + Output: ((tstz)::date), count(*) + Group Key: (slope_src.tstz)::date + -> Index Only Scan using slope_src_tstz_idx on public.slope_src + Output: (tstz)::date +(7 rows) + +-- +-- Test nested monotonic function +-- +-- floor(floor(x)) should still use index +explain (costs off, verbose) +select floor(floor(v_float8)), count(*) from slope_src group by 1; + QUERY PLAN +------------------------------------------------------------------------ + GroupAggregate + Output: (floor(floor(v_float8))), count(*) + Group Key: floor(floor(slope_src.v_float8)) + -> Index Only Scan using slope_src_v_float8_idx on public.slope_src + Output: floor(floor(v_float8)) +(5 rows) + +-- floor(v + 1) should use index +explain (costs off, verbose) +select floor(v_float8 + 1), count(*) from slope_src group by 1; + QUERY PLAN +------------------------------------------------------------------------ + GroupAggregate + Output: (floor((v_float8 + '1'::double precision))), count(*) + Group Key: floor((slope_src.v_float8 + '1'::double precision)) + -> Index Only Scan using slope_src_v_float8_idx on public.slope_src + Output: floor((v_float8 + '1'::double precision)) +(5 rows) + +-- +-- Test all index/query direction+nulls combinations for SLOPE. +-- For an increasing function like floor(), the scan uses the index when both +-- direction and nulls agree (Forward) or both are flipped (Backward). +-- If one agrees and the other disagrees, then the scan cannot use the +-- index and a sort is required. +-- +CREATE TABLE slope_nulls_tmp (v float8); +INSERT INTO slope_nulls_tmp VALUES (1), (NULL), (2); +ANALYZE slope_nulls_tmp; +CREATE TEMPORARY TABLE slope_nulls_results ( + seq serial, + sign text, index_order text, query_order text, scan_method text, + example text +); +DO $$ +DECLARE + r record; + plan_json json; + node_type text; + query text; + r1 text; + r2 text; +BEGIN + FOR r IN + SELECT idx_dir, idx_nf, qry_dir, qry_nf, sign + FROM unnest(ARRAY['ASC','DESC']) AS idx_dir, + unnest(ARRAY['FIRST','LAST']) AS idx_nf, + unnest(ARRAY['+','-']) AS sign, + unnest(ARRAY['FIRST','LAST']) AS qry_nf, + unnest(ARRAY['ASC','DESC']) AS qry_dir + LOOP + EXECUTE format('CREATE INDEX slope_nulls_tmp_idx ON slope_nulls_tmp (v %s NULLS %s)', + r.idx_dir, r.idx_nf); + query := format('SELECT floor(0.5 %s v) as x FROM slope_nulls_tmp ORDER BY 1 %s NULLS %s', + r.sign, r.qry_dir, r.qry_nf); + EXECUTE format('EXPLAIN (FORMAT JSON) %s', query) + INTO plan_json; + + SET enable_slope = off; + EXECUTE 'SELECT string_agg(coalesce(x::text, ''NULL''), '','') FROM (' || query || ') tmp(x)' into r1; + SET enable_slope = on; + EXECUTE 'SELECT string_agg(coalesce(x::text, ''NULL''), '','') FROM (' || query || ') tmp(x)' into r2; + if r1 <> r2 then + raise notice 'query %', query; + raise exception ' r1=%, r2=%', r1, r2; + end if; + node_type := plan_json->0->'Plan'->>'Node Type'; + INSERT INTO slope_nulls_results (sign, index_order, query_order, scan_method, example) VALUES ( + r.sign, + r.idx_dir || ' NULLS ' || r.idx_nf, + r.qry_dir || ' NULLS ' || r.qry_nf, + CASE WHEN node_type IN ('Index Only Scan', 'Index Scan') + THEN plan_json->0->'Plan'->>'Scan Direction' + ELSE node_type + END, + r2 + ); + EXECUTE format('DROP INDEX slope_nulls_tmp_idx'); + END LOOP; +END; +$$; +SELECT sign, index_order, query_order, scan_method, example +FROM slope_nulls_results ORDER BY seq; + sign | index_order | query_order | scan_method | example +------+------------------+------------------+-------------+------------ + + | ASC NULLS FIRST | ASC NULLS FIRST | Forward | NULL,1,2 + + | ASC NULLS LAST | ASC NULLS FIRST | Sort | NULL,1,2 + + | DESC NULLS FIRST | ASC NULLS FIRST | Sort | NULL,1,2 + + | DESC NULLS LAST | ASC NULLS FIRST | Backward | NULL,1,2 + - | ASC NULLS FIRST | ASC NULLS FIRST | Sort | NULL,-2,-1 + - | ASC NULLS LAST | ASC NULLS FIRST | Sort | NULL,-2,-1 + - | DESC NULLS FIRST | ASC NULLS FIRST | Sort | NULL,-2,-1 + - | DESC NULLS LAST | ASC NULLS FIRST | Sort | NULL,-2,-1 + + | ASC NULLS FIRST | ASC NULLS LAST | Sort | 1,2,NULL + + | ASC NULLS LAST | ASC NULLS LAST | Forward | 1,2,NULL + + | DESC NULLS FIRST | ASC NULLS LAST | Backward | 1,2,NULL + + | DESC NULLS LAST | ASC NULLS LAST | Sort | 1,2,NULL + - | ASC NULLS FIRST | ASC NULLS LAST | Sort | -2,-1,NULL + - | ASC NULLS LAST | ASC NULLS LAST | Sort | -2,-1,NULL + - | DESC NULLS FIRST | ASC NULLS LAST | Sort | -2,-1,NULL + - | DESC NULLS LAST | ASC NULLS LAST | Sort | -2,-1,NULL + + | ASC NULLS FIRST | DESC NULLS FIRST | Sort | NULL,2,1 + + | ASC NULLS LAST | DESC NULLS FIRST | Backward | NULL,2,1 + + | DESC NULLS FIRST | DESC NULLS FIRST | Forward | NULL,2,1 + + | DESC NULLS LAST | DESC NULLS FIRST | Sort | NULL,2,1 + - | ASC NULLS FIRST | DESC NULLS FIRST | Sort | NULL,-1,-2 + - | ASC NULLS LAST | DESC NULLS FIRST | Sort | NULL,-1,-2 + - | DESC NULLS FIRST | DESC NULLS FIRST | Sort | NULL,-1,-2 + - | DESC NULLS LAST | DESC NULLS FIRST | Sort | NULL,-1,-2 + + | ASC NULLS FIRST | DESC NULLS LAST | Backward | 2,1,NULL + + | ASC NULLS LAST | DESC NULLS LAST | Sort | 2,1,NULL + + | DESC NULLS FIRST | DESC NULLS LAST | Sort | 2,1,NULL + + | DESC NULLS LAST | DESC NULLS LAST | Forward | 2,1,NULL + - | ASC NULLS FIRST | DESC NULLS LAST | Sort | -1,-2,NULL + - | ASC NULLS LAST | DESC NULLS LAST | Sort | -1,-2,NULL + - | DESC NULLS FIRST | DESC NULLS LAST | Sort | -1,-2,NULL + - | DESC NULLS LAST | DESC NULLS LAST | Sort | -1,-2,NULL +(32 rows) + +DROP TABLE slope_nulls_results; +DROP TABLE slope_nulls_tmp; +-- +-- Directed domain test +-- +CREATE DOMAIN df AS float8; +CREATE TABLE t (id int, x df); +INSERT INTO t VALUES (1,'-inf'),(2,'-1'),(3,'0'),(4,'3'),(5,'inf'),(6,'nan'); +CREATE INDEX ON t (x); +SELECT id, -x AS f FROM t ORDER BY (-x) ASC NULLS FIRST; + id | f +----+----------- + 5 | -Infinity + 4 | -3 + 3 | -0 + 2 | 1 + 1 | Infinity + 6 | NaN +(6 rows) + +DROP TABLE t; +DROP DOMAIN df; +-- +-- Test special numeric values +-- +-- This check the correctness of the query for a nullable column +-- also containing special values (-inf, +inf and NaN) +-- represented as float8, float4, numeric and user defined +-- domains with float8 base type. +CREATE DOMAIN non42 AS float8 CHECK (value != 42); +CREATE DOMAIN fp_real AS float8; +CREATE UNLOGGED TABLE slope_numeric_corners (i serial, x float8); +INSERT INTO slope_numeric_corners (x) +SELECT x::float8 as x FROM + unnest(ARRAY['-inf', '-1', '0', '3', 'inf', 'nan', NULL]) x(x) +ORDER BY 1; +CREATE INDEX slope_numeric_corners_xfp8_idx_nulllast ON slope_numeric_corners ((x::float8) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xfp8_idx_nullfirst ON slope_numeric_corners ((x::float8) ASC NULLS FIRST); +CREATE INDEX slope_numeric_corners_xfp4_idx_nulllast ON slope_numeric_corners ((x::float4) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xfp4_idx_nullfirst ON slope_numeric_corners ((x::float4) ASC NULLS FIRST); +CREATE INDEX slope_numeric_corners_xnum_idx_nulllast ON slope_numeric_corners ((x::numeric) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xnum_idx_nullfirst ON slope_numeric_corners ((x::numeric) ASC NULLS FIRST); +CREATE INDEX slope_numeric_corners_xnon42_idx_nulllast ON slope_numeric_corners ((x::non42) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xnon42_idx_nullfirst ON slope_numeric_corners ((x::non42) ASC NULLS FIRST); +CREATE INDEX slope_numeric_corners_xfpreal_idx_nulllast ON slope_numeric_corners ((x::fp_real) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xfpreal_idx_nullfirst ON slope_numeric_corners ((x::fp_real) ASC NULLS FIRST); +CREATE TEMPORARY TABLE slope_numeric_corners_results ( + seq serial, + expr text COLLATE "C", + sort_order text COLLATE "C", + nulls_order text COLLATE "C", + typename text COLLATE "C", + result float8[], + expected float8[], + expected_seq int4[], + nan_values float8[], + plan1_json json, + plan2_json json +); +DO $$ +DECLARE + r record; + result float8[]; + expected float8[]; + expected_seq int4[]; + nan_values float8[]; + query text; + agg_query text; + plan_query text; + nan_query text; + expected_seq_query text; + plan1_json json; + plan2_json json; +BEGIN + + FOR r IN + SELECT replace(replace(s, 'x', 'x::' || t), 'A', a || '::' || t) as expr, sort_order, nulls_order, t as typename + FROM + unnest(ARRAY['ASC', 'DESC']) WITH ORDINALITY AS so(sort_order, so_i), + unnest(ARRAY['FIRST', 'LAST']) WITH ORDINALITY AS nf(nulls_order, no_i), + unnest(ARRAY['float8', 'float4', 'numeric', 'non42', 'fp_real']) WITH ORDINALITY AS t(t, t_i), + unnest(ARRAY[ + 'x+A', 'x-A', 'x*A', 'x/A', + 'A+x', 'A-x', 'A*x', '-x' + ]) WITH ORDINALITY AS s(s, s_i), + unnest(ARRAY['''inf''', '''-inf''', '1']) WITH ORDINALITY AS a(a, a_i) + ORDER BY s_i, so_i, no_i, t_i + LOOP + query := 'SELECT *, (' || r.expr || ') as f ' + || 'FROM slope_numeric_corners ' + || 'ORDER BY f ' || r.sort_order || ' NULLS ' || r.nulls_order; + nan_query := 'SELECT array_agg(x) FROM slope_numeric_corners + WHERE ' || r.expr || ' = ''nan''::' || r.typename || ' AND x != ''nan''::' || r.typename; + agg_query := 'SELECT array_agg(f::float8) FROM (' || query || ') tmp'; + expected_seq_query := 'SELECT array_agg(i::int4) FROM (' || query || ') tmp'; + plan_query := 'EXPLAIN (FORMAT JSON) ' || query; + -- slope optimization disabled + SET enable_slope = off; + EXECUTE agg_query into result; + EXECUTE plan_query into plan2_json; + -- slope optimization enabled + SET enable_slope = on; + EXECUTE agg_query into expected; + EXECUTE expected_seq_query into expected_seq; + EXECUTE nan_query into nan_values; + EXECUTE plan_query into plan1_json; + -- store results + INSERT INTO slope_numeric_corners_results + (expr, sort_order, nulls_order, typename, result, expected, expected_seq, nan_values, plan1_json, plan2_json) + VALUES (r.expr, r.sort_order, r.nulls_order, r.typename, result, expected, expected_seq, nan_values, plan1_json, plan2_json); + END LOOP; +END; +$$; +-- display failing test cases +SELECT seq, expr +, sort_order, nulls_order +, expected, expected_seq, result, nan_values +, plan2_json->0->'Plan'->>'Node Type' as plan2 +FROM slope_numeric_corners_results +WHERE expected != result; + seq | expr | sort_order | nulls_order | expected | expected_seq | result | nan_values | plan2 +-----+------+------------+-------------+----------+--------------+--------+------------+------- +(0 rows) + +-- check the number of test cases +SELECT expected = result as passed, count(1) +FROM slope_numeric_corners_results +GROUP BY 1; + passed | count +--------+------- + t | 480 +(1 row) + +DROP TABLE slope_numeric_corners; +DROP TABLE slope_numeric_corners_results; diff --git a/src/test/regress/sql/slope.sql b/src/test/regress/sql/slope.sql index e2f4cfa2727..ef7476c0606 100644 --- a/src/test/regress/sql/slope.sql +++ b/src/test/regress/sql/slope.sql @@ -1,14 +1,29 @@ +-- Configure parameters to avoid competing plans. + +-- Prevent seqscans or bitmapscans that could be combined +-- with a sort as an alternative to an index scan that +-- satisfies the query order. +SET enable_seqscan = off; +SET enable_bitmapscan = off; +-- Every query should rely on an index, and a scan without +-- a sort will always be cheaper than a scan with a sort. +SET enable_indexscan = on; +SET enable_indexonlyscan = on; +-- Disable hashagg that could provide an alterntative to a +-- GROUP BY that doesn't require the input to be sorted. +SET enable_hashagg = off; + +-- +-- Check the ability to use index scans when the index +-- for different nulls order, on either nullable or non-nullable columns. +-- CREATE TABLE slope_t1 (a int, b int NOT NULL); INSERT INTO slope_t1 SELECT i, i FROM generate_series(1, 10) AS i; CREATE INDEX slope_t1_a_idx ON slope_t1 (a); CREATE INDEX slope_t1_b_idx ON slope_t1 (b); - ANALYSE slope_t1; -SET enable_seqscan = off; -SET enable_bitmapscan = off; - -- Column `a` can be scanned in two directions, but the -- nulls order of the query must match the nulls order of the index. EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 ASC NULLS FIRST; @@ -22,11 +37,370 @@ EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 ASC NULLS LAST; EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 DESC NULLS FIRST; EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 DESC NULLS LAST; - -- a query where a is not null by an equivalence class EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 ASC NULLS FIRST; EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 ASC NULLS LAST; EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 DESC NULLS FIRST; EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 DESC NULLS LAST; -DROP TABLE slope_t1; \ No newline at end of file +DROP TABLE slope_t1; + + +-- +-- SLOPE (Scalar function Leveraging Ordered Path Evaluation) +-- Test that monotonic functions can use indexes for ordering +-- + +-- Create test table with various data types +CREATE TABLE slope_src ( + id serial PRIMARY KEY, + v_int2 int2, + v_int4 int4, + v_int8 int8, + v_float4 float4, + v_float8 float8, + v_numeric numeric, + ts timestamp, + tstz timestamptz +); + +-- Insert some test data +INSERT INTO slope_src (v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz) +SELECT + (i % 100)::int2, + i, + i::int8, + i::float4, + i::float8, + i::numeric, + '2020-01-01'::timestamp + (i || ' hours')::interval, + '2020-01-01'::timestamptz + (i || ' hours')::interval +FROM generate_series(1, 1000) i; + +-- Create indexes on the columns we'll test +CREATE INDEX slope_src_v_int4_idx ON slope_src (v_int4); +CREATE INDEX slope_src_v_int8_idx ON slope_src (v_int8); +CREATE INDEX slope_src_v_float8_idx ON slope_src (v_float8); +CREATE INDEX slope_src_v_numeric_idx ON slope_src (v_numeric); +CREATE INDEX slope_src_ts_idx ON slope_src (ts); +CREATE INDEX slope_src_tstz_idx ON slope_src (tstz); + +-- Analyze to get good statistics +ANALYZE slope_src; + +-- +-- Test that SLOPE is enabled and can be disabled +-- + +SET enable_slope = off; +-- Basic: floor(float8) should sort even though the index order +-- produces the result in the correct order. +explain (costs off, verbose) +select floor(v_float8), count(*) from slope_src group by 1; +RESET enable_slope; +SHOW enable_slope; + +-- +-- Test GROUP BY with monotonic function +-- + +-- Basic: floor(float8) should use index on v_float8 +explain (costs off, verbose) +select floor(v_float8), count(*) from slope_src group by 1; + +-- ceil(float8) should use index on v_float8 +explain (costs off, verbose) +select ceil(v_float8), count(*) from slope_src group by 1; + +-- floor(numeric) should use index on v_numeric +explain (costs off, verbose) +select floor(v_numeric), count(*) from slope_src group by 1; + +-- timestamp::date cast should use index on ts +explain (costs off, verbose) +select ts::date, count(*) from slope_src group by 1; + +-- date_trunc on timestamp should use index +explain (costs off, verbose) +select date_trunc('day', ts), count(*) from slope_src group by 1; + +-- date_trunc on timestamptz should use index +explain (costs off, verbose) +select date_trunc('day', tstz), count(*) from slope_src group by 1; + + +-- +-- Test arithmetic operations +-- + +-- Addition: v_int4 + 10 is increasing in v_int4 +explain (costs off, verbose) +select v_int4 + 10, count(*) from slope_src group by 1; + +-- Subtraction: v_int4 - 10 is increasing in v_int4 +explain (costs off, verbose) +select v_int4 - 10, count(*) from slope_src group by 1; + +-- Multiplication by positive constant: v_int4 * 2 is increasing +explain (costs off, verbose) +select v_int4 * 2, count(*) from slope_src group by 1; + +-- Division by positive constant: v_int4 / 2 is increasing +explain (costs off, verbose) +select v_int4 / 2, count(*) from slope_src group by 1; + + +-- +-- Test decreasing functions +-- These queries can't use the index order because group pathkeys +-- are always ASC NULLS LAST, while -int_v4 is DESC NULLS LAST. + +-- Unary minus: -v_int4 is decreasing in v_int4 +explain (costs off, verbose) +select -v_int4, count(*) from slope_src group by 1; + +-- Subtraction from constant: 1000 - v_int4 is decreasing in v_int4 +explain (costs off, verbose) +select 1000 - v_int4, count(*) from slope_src group by 1; + +-- Multiplication by negative constant: v_int4 * (-2) is decreasing +explain (costs off, verbose) +select v_int4 * (-2), count(*) from slope_src group by 1; + +-- Division by negative constant: v_int4 / (-2) is decreasing +explain (costs off, verbose) +select v_int4 / (-2), count(*) from slope_src group by 1; + +-- +-- Test ORDER BY with monotonic function +-- + +-- ORDER BY floor(v_float8) should use index +explain (costs off, verbose) +select floor(v_float8), v_float8 from slope_src order by 1 limit 10; + +-- ORDER BY -v_int4 DESC requires sorting +-- the index order is v_int4 ASC and, implicitly, NULLS LAST +-- a forward scan gives -v_int4 DESC NULLS LAST +-- a backward scan gives -v_int4 ASC NULLS FIRST +-- the query order is DESC and, implicitly, NULLS FIRST +explain (costs off, verbose) +select -v_int4 from slope_src order by 1 desc limit 10; + +-- ORDER BY -v_int4 ASC requires sorting +-- a forward scan gives -v_int4 DESC NULLS LAST +-- a backward scan gives -v_int4 ASC NULLS FIRST +-- the query order is -v_int4 ASC NULLS LAST +explain (costs off, verbose) +select -v_int4 from slope_src order by 1 limit 10; + +-- +-- Group and order +-- + +explain (costs off, verbose) +select tstz::date, count(*) from slope_src group by 1 order by 1 limit 10; + +-- +-- Test nested monotonic function +-- + +-- floor(floor(x)) should still use index +explain (costs off, verbose) +select floor(floor(v_float8)), count(*) from slope_src group by 1; + +-- floor(v + 1) should use index +explain (costs off, verbose) +select floor(v_float8 + 1), count(*) from slope_src group by 1; + +-- +-- Test all index/query direction+nulls combinations for SLOPE. +-- For an increasing function like floor(), the scan uses the index when both +-- direction and nulls agree (Forward) or both are flipped (Backward). +-- If one agrees and the other disagrees, then the scan cannot use the +-- index and a sort is required. +-- +CREATE TABLE slope_nulls_tmp (v float8); +INSERT INTO slope_nulls_tmp VALUES (1), (NULL), (2); +ANALYZE slope_nulls_tmp; + +CREATE TEMPORARY TABLE slope_nulls_results ( + seq serial, + sign text, index_order text, query_order text, scan_method text, + example text +); +DO $$ +DECLARE + r record; + plan_json json; + node_type text; + query text; + r1 text; + r2 text; +BEGIN + FOR r IN + SELECT idx_dir, idx_nf, qry_dir, qry_nf, sign + FROM unnest(ARRAY['ASC','DESC']) AS idx_dir, + unnest(ARRAY['FIRST','LAST']) AS idx_nf, + unnest(ARRAY['+','-']) AS sign, + unnest(ARRAY['FIRST','LAST']) AS qry_nf, + unnest(ARRAY['ASC','DESC']) AS qry_dir + LOOP + EXECUTE format('CREATE INDEX slope_nulls_tmp_idx ON slope_nulls_tmp (v %s NULLS %s)', + r.idx_dir, r.idx_nf); + query := format('SELECT floor(0.5 %s v) as x FROM slope_nulls_tmp ORDER BY 1 %s NULLS %s', + r.sign, r.qry_dir, r.qry_nf); + EXECUTE format('EXPLAIN (FORMAT JSON) %s', query) + INTO plan_json; + + SET enable_slope = off; + EXECUTE 'SELECT string_agg(coalesce(x::text, ''NULL''), '','') FROM (' || query || ') tmp(x)' into r1; + SET enable_slope = on; + EXECUTE 'SELECT string_agg(coalesce(x::text, ''NULL''), '','') FROM (' || query || ') tmp(x)' into r2; + if r1 <> r2 then + raise notice 'query %', query; + raise exception ' r1=%, r2=%', r1, r2; + end if; + node_type := plan_json->0->'Plan'->>'Node Type'; + INSERT INTO slope_nulls_results (sign, index_order, query_order, scan_method, example) VALUES ( + r.sign, + r.idx_dir || ' NULLS ' || r.idx_nf, + r.qry_dir || ' NULLS ' || r.qry_nf, + CASE WHEN node_type IN ('Index Only Scan', 'Index Scan') + THEN plan_json->0->'Plan'->>'Scan Direction' + ELSE node_type + END, + r2 + ); + EXECUTE format('DROP INDEX slope_nulls_tmp_idx'); + END LOOP; +END; +$$; + +SELECT sign, index_order, query_order, scan_method, example +FROM slope_nulls_results ORDER BY seq; +DROP TABLE slope_nulls_results; +DROP TABLE slope_nulls_tmp; + + +-- +-- Directed domain test +-- +CREATE DOMAIN df AS float8; +CREATE TABLE t (id int, x df); +INSERT INTO t VALUES (1,'-inf'),(2,'-1'),(3,'0'),(4,'3'),(5,'inf'),(6,'nan'); +CREATE INDEX ON t (x); +SELECT id, -x AS f FROM t ORDER BY (-x) ASC NULLS FIRST; +DROP TABLE t; +DROP DOMAIN df; + + +-- +-- Test special numeric values +-- +-- This check the correctness of the query for a nullable column +-- also containing special values (-inf, +inf and NaN) +-- represented as float8, float4, numeric and user defined +-- domains with float8 base type. +CREATE DOMAIN non42 AS float8 CHECK (value != 42); +CREATE DOMAIN fp_real AS float8; +CREATE UNLOGGED TABLE slope_numeric_corners (i serial, x float8); +INSERT INTO slope_numeric_corners (x) +SELECT x::float8 as x FROM + unnest(ARRAY['-inf', '-1', '0', '3', 'inf', 'nan', NULL]) x(x) +ORDER BY 1; + +CREATE INDEX slope_numeric_corners_xfp8_idx_nulllast ON slope_numeric_corners ((x::float8) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xfp8_idx_nullfirst ON slope_numeric_corners ((x::float8) ASC NULLS FIRST); +CREATE INDEX slope_numeric_corners_xfp4_idx_nulllast ON slope_numeric_corners ((x::float4) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xfp4_idx_nullfirst ON slope_numeric_corners ((x::float4) ASC NULLS FIRST); +CREATE INDEX slope_numeric_corners_xnum_idx_nulllast ON slope_numeric_corners ((x::numeric) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xnum_idx_nullfirst ON slope_numeric_corners ((x::numeric) ASC NULLS FIRST); +CREATE INDEX slope_numeric_corners_xnon42_idx_nulllast ON slope_numeric_corners ((x::non42) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xnon42_idx_nullfirst ON slope_numeric_corners ((x::non42) ASC NULLS FIRST); +CREATE INDEX slope_numeric_corners_xfpreal_idx_nulllast ON slope_numeric_corners ((x::fp_real) ASC NULLS LAST); +CREATE INDEX slope_numeric_corners_xfpreal_idx_nullfirst ON slope_numeric_corners ((x::fp_real) ASC NULLS FIRST); + +CREATE TEMPORARY TABLE slope_numeric_corners_results ( + seq serial, + expr text COLLATE "C", + sort_order text COLLATE "C", + nulls_order text COLLATE "C", + typename text COLLATE "C", + result float8[], + expected float8[], + expected_seq int4[], + nan_values float8[], + plan1_json json, + plan2_json json +); +DO $$ +DECLARE + r record; + result float8[]; + expected float8[]; + expected_seq int4[]; + nan_values float8[]; + query text; + agg_query text; + plan_query text; + nan_query text; + expected_seq_query text; + plan1_json json; + plan2_json json; +BEGIN + + FOR r IN + SELECT replace(replace(s, 'x', 'x::' || t), 'A', a || '::' || t) as expr, sort_order, nulls_order, t as typename + FROM + unnest(ARRAY['ASC', 'DESC']) WITH ORDINALITY AS so(sort_order, so_i), + unnest(ARRAY['FIRST', 'LAST']) WITH ORDINALITY AS nf(nulls_order, no_i), + unnest(ARRAY['float8', 'float4', 'numeric', 'non42', 'fp_real']) WITH ORDINALITY AS t(t, t_i), + unnest(ARRAY[ + 'x+A', 'x-A', 'x*A', 'x/A', + 'A+x', 'A-x', 'A*x', '-x' + ]) WITH ORDINALITY AS s(s, s_i), + unnest(ARRAY['''inf''', '''-inf''', '1']) WITH ORDINALITY AS a(a, a_i) + ORDER BY s_i, so_i, no_i, t_i + LOOP + query := 'SELECT *, (' || r.expr || ') as f ' + || 'FROM slope_numeric_corners ' + || 'ORDER BY f ' || r.sort_order || ' NULLS ' || r.nulls_order; + nan_query := 'SELECT array_agg(x) FROM slope_numeric_corners + WHERE ' || r.expr || ' = ''nan''::' || r.typename || ' AND x != ''nan''::' || r.typename; + agg_query := 'SELECT array_agg(f::float8) FROM (' || query || ') tmp'; + expected_seq_query := 'SELECT array_agg(i::int4) FROM (' || query || ') tmp'; + plan_query := 'EXPLAIN (FORMAT JSON) ' || query; + -- slope optimization disabled + SET enable_slope = off; + EXECUTE agg_query into result; + EXECUTE plan_query into plan2_json; + -- slope optimization enabled + SET enable_slope = on; + EXECUTE agg_query into expected; + EXECUTE expected_seq_query into expected_seq; + EXECUTE nan_query into nan_values; + EXECUTE plan_query into plan1_json; + -- store results + INSERT INTO slope_numeric_corners_results + (expr, sort_order, nulls_order, typename, result, expected, expected_seq, nan_values, plan1_json, plan2_json) + VALUES (r.expr, r.sort_order, r.nulls_order, r.typename, result, expected, expected_seq, nan_values, plan1_json, plan2_json); + END LOOP; +END; +$$; + +-- display failing test cases +SELECT seq, expr +, sort_order, nulls_order +, expected, expected_seq, result, nan_values +, plan2_json->0->'Plan'->>'Node Type' as plan2 +FROM slope_numeric_corners_results +WHERE expected != result; + +-- check the number of test cases +SELECT expected = result as passed, count(1) +FROM slope_numeric_corners_results +GROUP BY 1; + +DROP TABLE slope_numeric_corners; +DROP TABLE slope_numeric_corners_results; \ No newline at end of file -- 2.53.0