Skip to content

Commit 075df6b

Browse files
committed
Add planner support functions for range operators <@ and @>.
These support functions will transform expressions with constant range values into direct comparisons on the range bound values, which are frequently better-optimizable. The transformation is skipped however if it would require double evaluation of a volatile or expensive element expression. Along the way, add the range opfamily OID to range typcache entries, since load_rangetype_info has to compute that anyway and it seems silly to duplicate the work later. Kim Johan Andersson and Jian He, reviewed by Laurenz Albe Discussion: https://postgr.es/m/94f64d1f-b8c0-b0c5-98bc-0793a34e0851@kimmet.dk
1 parent abb0b4f commit 075df6b

File tree

8 files changed

+469
-11
lines changed

8 files changed

+469
-11
lines changed

src/backend/utils/adt/rangetypes.c

+240-4
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,20 @@
3030
*/
3131
#include "postgres.h"
3232

33-
#include "access/tupmacs.h"
3433
#include "common/hashfn.h"
35-
#include "lib/stringinfo.h"
3634
#include "libpq/pqformat.h"
3735
#include "miscadmin.h"
36+
#include "nodes/makefuncs.h"
3837
#include "nodes/miscnodes.h"
39-
#include "port/pg_bitutils.h"
38+
#include "nodes/supportnodes.h"
39+
#include "optimizer/clauses.h"
40+
#include "optimizer/cost.h"
41+
#include "optimizer/optimizer.h"
4042
#include "utils/builtins.h"
4143
#include "utils/date.h"
4244
#include "utils/lsyscache.h"
4345
#include "utils/rangetypes.h"
4446
#include "utils/timestamp.h"
45-
#include "varatt.h"
4647

4748

4849
/* fn_extra cache entry for one of the range I/O functions */
@@ -69,6 +70,12 @@ static Size datum_compute_size(Size data_length, Datum val, bool typbyval,
6970
char typalign, int16 typlen, char typstorage);
7071
static Pointer datum_write(Pointer ptr, Datum datum, bool typbyval,
7172
char typalign, int16 typlen, char typstorage);
73+
static Node *find_simplified_clause(PlannerInfo *root,
74+
Expr *rangeExpr, Expr *elemExpr);
75+
static Expr *build_bound_expr(Expr *elemExpr, Datum val,
76+
bool isLowerBound, bool isInclusive,
77+
TypeCacheEntry *typeCache,
78+
Oid opfamily, Oid rng_collation);
7279

7380

7481
/*
@@ -2173,6 +2180,58 @@ make_empty_range(TypeCacheEntry *typcache)
21732180
return make_range(typcache, &lower, &upper, true, NULL);
21742181
}
21752182

2183+
/*
2184+
* Planner support function for elem_contained_by_range (<@ operator).
2185+
*/
2186+
Datum
2187+
elem_contained_by_range_support(PG_FUNCTION_ARGS)
2188+
{
2189+
Node *rawreq = (Node *) PG_GETARG_POINTER(0);
2190+
Node *ret = NULL;
2191+
2192+
if (IsA(rawreq, SupportRequestSimplify))
2193+
{
2194+
SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
2195+
FuncExpr *fexpr = req->fcall;
2196+
Expr *leftop,
2197+
*rightop;
2198+
2199+
Assert(list_length(fexpr->args) == 2);
2200+
leftop = linitial(fexpr->args);
2201+
rightop = lsecond(fexpr->args);
2202+
2203+
ret = find_simplified_clause(req->root, rightop, leftop);
2204+
}
2205+
2206+
PG_RETURN_POINTER(ret);
2207+
}
2208+
2209+
/*
2210+
* Planner support function for range_contains_elem (@> operator).
2211+
*/
2212+
Datum
2213+
range_contains_elem_support(PG_FUNCTION_ARGS)
2214+
{
2215+
Node *rawreq = (Node *) PG_GETARG_POINTER(0);
2216+
Node *ret = NULL;
2217+
2218+
if (IsA(rawreq, SupportRequestSimplify))
2219+
{
2220+
SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
2221+
FuncExpr *fexpr = req->fcall;
2222+
Expr *leftop,
2223+
*rightop;
2224+
2225+
Assert(list_length(fexpr->args) == 2);
2226+
leftop = linitial(fexpr->args);
2227+
rightop = lsecond(fexpr->args);
2228+
2229+
ret = find_simplified_clause(req->root, leftop, rightop);
2230+
}
2231+
2232+
PG_RETURN_POINTER(ret);
2233+
}
2234+
21762235

21772236
/*
21782237
*----------------------------------------------------------
@@ -2715,3 +2774,180 @@ datum_write(Pointer ptr, Datum datum, bool typbyval, char typalign,
27152774

27162775
return ptr;
27172776
}
2777+
2778+
/*
2779+
* Common code for the elem_contained_by_range and range_contains_elem
2780+
* support functions. The caller has extracted the function argument
2781+
* expressions, and swapped them if necessary to pass the range first.
2782+
*
2783+
* Returns a simplified replacement expression, or NULL if we can't simplify.
2784+
*/
2785+
static Node *
2786+
find_simplified_clause(PlannerInfo *root, Expr *rangeExpr, Expr *elemExpr)
2787+
{
2788+
RangeType *range;
2789+
TypeCacheEntry *rangetypcache;
2790+
RangeBound lower;
2791+
RangeBound upper;
2792+
bool empty;
2793+
2794+
/* can't do anything unless the range is a non-null constant */
2795+
if (!IsA(rangeExpr, Const) || ((Const *) rangeExpr)->constisnull)
2796+
return NULL;
2797+
range = DatumGetRangeTypeP(((Const *) rangeExpr)->constvalue);
2798+
2799+
rangetypcache = lookup_type_cache(RangeTypeGetOid(range),
2800+
TYPECACHE_RANGE_INFO);
2801+
if (rangetypcache->rngelemtype == NULL)
2802+
elog(ERROR, "type %u is not a range type", RangeTypeGetOid(range));
2803+
2804+
range_deserialize(rangetypcache, range, &lower, &upper, &empty);
2805+
2806+
if (empty)
2807+
{
2808+
/* if the range is empty, then there can be no matches */
2809+
return makeBoolConst(false, false);
2810+
}
2811+
else if (lower.infinite && upper.infinite)
2812+
{
2813+
/* the range has infinite bounds, so it matches everything */
2814+
return makeBoolConst(true, false);
2815+
}
2816+
else
2817+
{
2818+
/* at least one bound is available, we have something to work with */
2819+
TypeCacheEntry *elemTypcache = rangetypcache->rngelemtype;
2820+
Oid opfamily = rangetypcache->rng_opfamily;
2821+
Oid rng_collation = rangetypcache->rng_collation;
2822+
Expr *lowerExpr = NULL;
2823+
Expr *upperExpr = NULL;
2824+
2825+
if (!lower.infinite && !upper.infinite)
2826+
{
2827+
/*
2828+
* When both bounds are present, we have a problem: the
2829+
* "simplified" clause would need to evaluate the elemExpr twice.
2830+
* That's definitely not okay if the elemExpr is volatile, and
2831+
* it's also unattractive if the elemExpr is expensive.
2832+
*/
2833+
QualCost eval_cost;
2834+
2835+
if (contain_volatile_functions((Node *) elemExpr))
2836+
return NULL;
2837+
2838+
/*
2839+
* We define "expensive" as "contains any subplan or more than 10
2840+
* operators". Note that the subplan search has to be done
2841+
* explicitly, since cost_qual_eval() will barf on unplanned
2842+
* subselects.
2843+
*/
2844+
if (contain_subplans((Node *) elemExpr))
2845+
return NULL;
2846+
cost_qual_eval_node(&eval_cost, (Node *) elemExpr, root);
2847+
if (eval_cost.startup + eval_cost.per_tuple >
2848+
10 * cpu_operator_cost)
2849+
return NULL;
2850+
}
2851+
2852+
/* Okay, try to build boundary comparison expressions */
2853+
if (!lower.infinite)
2854+
{
2855+
lowerExpr = build_bound_expr(elemExpr,
2856+
lower.val,
2857+
true,
2858+
lower.inclusive,
2859+
elemTypcache,
2860+
opfamily,
2861+
rng_collation);
2862+
if (lowerExpr == NULL)
2863+
return NULL;
2864+
}
2865+
2866+
if (!upper.infinite)
2867+
{
2868+
/* Copy the elemExpr if we need two copies */
2869+
if (!lower.infinite)
2870+
elemExpr = copyObject(elemExpr);
2871+
upperExpr = build_bound_expr(elemExpr,
2872+
upper.val,
2873+
false,
2874+
upper.inclusive,
2875+
elemTypcache,
2876+
opfamily,
2877+
rng_collation);
2878+
if (upperExpr == NULL)
2879+
return NULL;
2880+
}
2881+
2882+
if (lowerExpr != NULL && upperExpr != NULL)
2883+
return (Node *) make_andclause(list_make2(lowerExpr, upperExpr));
2884+
else if (lowerExpr != NULL)
2885+
return (Node *) lowerExpr;
2886+
else if (upperExpr != NULL)
2887+
return (Node *) upperExpr;
2888+
else
2889+
{
2890+
Assert(false);
2891+
return NULL;
2892+
}
2893+
}
2894+
}
2895+
2896+
/*
2897+
* Helper function for find_simplified_clause().
2898+
*
2899+
* Build the expression (elemExpr Operator val), where the operator is
2900+
* the appropriate member of the given opfamily depending on
2901+
* isLowerBound and isInclusive. typeCache is the typcache entry for
2902+
* the "val" value (presently, this will be the same type as elemExpr).
2903+
* rng_collation is the collation to use in the comparison.
2904+
*
2905+
* Return NULL on failure (if, for some reason, we can't find the operator).
2906+
*/
2907+
static Expr *
2908+
build_bound_expr(Expr *elemExpr, Datum val,
2909+
bool isLowerBound, bool isInclusive,
2910+
TypeCacheEntry *typeCache,
2911+
Oid opfamily, Oid rng_collation)
2912+
{
2913+
Oid elemType = typeCache->type_id;
2914+
int16 elemTypeLen = typeCache->typlen;
2915+
bool elemByValue = typeCache->typbyval;
2916+
Oid elemCollation = typeCache->typcollation;
2917+
int16 strategy;
2918+
Oid oproid;
2919+
Expr *constExpr;
2920+
2921+
/* Identify the comparison operator to use */
2922+
if (isLowerBound)
2923+
strategy = isInclusive ? BTGreaterEqualStrategyNumber : BTGreaterStrategyNumber;
2924+
else
2925+
strategy = isInclusive ? BTLessEqualStrategyNumber : BTLessStrategyNumber;
2926+
2927+
/*
2928+
* We could use exprType(elemExpr) here, if it ever becomes possible that
2929+
* elemExpr is not the exact same type as the range elements.
2930+
*/
2931+
oproid = get_opfamily_member(opfamily, elemType, elemType, strategy);
2932+
2933+
/* We don't really expect failure here, but just in case ... */
2934+
if (!OidIsValid(oproid))
2935+
return NULL;
2936+
2937+
/* OK, convert "val" to a full-fledged Const node, and make the OpExpr */
2938+
constExpr = (Expr *) makeConst(elemType,
2939+
-1,
2940+
elemCollation,
2941+
elemTypeLen,
2942+
val,
2943+
false,
2944+
elemByValue);
2945+
2946+
return make_opclause(oproid,
2947+
BOOLOID,
2948+
false,
2949+
elemExpr,
2950+
constExpr,
2951+
InvalidOid,
2952+
rng_collation);
2953+
}

src/backend/utils/adt/rangetypes_selfuncs.c

+4-3
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,10 @@ rangesel(PG_FUNCTION_ARGS)
196196
else if (operator == OID_RANGE_ELEM_CONTAINED_OP)
197197
{
198198
/*
199-
* Here, the Var is the elem, not the range. For now we just punt and
200-
* return the default estimate. In future we could disassemble the
201-
* range constant and apply scalarineqsel ...
199+
* Here, the Var is the elem, not the range. In typical cases
200+
* elem_contained_by_range_support will have simplified this case, so
201+
* that we won't get here. If we do get here we'll fall back on a
202+
* default estimate.
202203
*/
203204
}
204205
else if (((Const *) other)->consttype == vardata.vartype)

src/backend/utils/cache/typcache.c

+1
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,7 @@ load_rangetype_info(TypeCacheEntry *typentry)
940940
/* get opclass properties and look up the comparison function */
941941
opfamilyOid = get_opclass_family(opclassOid);
942942
opcintype = get_opclass_input_type(opclassOid);
943+
typentry->rng_opfamily = opfamilyOid;
943944

944945
cmpFnOid = get_opfamily_proc(opfamilyOid, opcintype, opcintype,
945946
BTORDER_PROC);

src/include/catalog/catversion.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@
5757
*/
5858

5959
/* yyyymmddN */
60-
#define CATALOG_VERSION_NO 202401191
60+
#define CATALOG_VERSION_NO 202401201
6161

6262
#endif

src/include/catalog/pg_proc.dat

+11-3
Original file line numberDiff line numberDiff line change
@@ -10503,13 +10503,15 @@
1050310503
proname => 'range_overlaps', prorettype => 'bool',
1050410504
proargtypes => 'anyrange anyrange', prosrc => 'range_overlaps' },
1050510505
{ oid => '3858',
10506-
proname => 'range_contains_elem', prorettype => 'bool',
10507-
proargtypes => 'anyrange anyelement', prosrc => 'range_contains_elem' },
10506+
proname => 'range_contains_elem', prosupport => 'range_contains_elem_support',
10507+
prorettype => 'bool', proargtypes => 'anyrange anyelement',
10508+
prosrc => 'range_contains_elem' },
1050810509
{ oid => '3859',
1050910510
proname => 'range_contains', prorettype => 'bool',
1051010511
proargtypes => 'anyrange anyrange', prosrc => 'range_contains' },
1051110512
{ oid => '3860',
10512-
proname => 'elem_contained_by_range', prorettype => 'bool',
10513+
proname => 'elem_contained_by_range',
10514+
prosupport => 'elem_contained_by_range_support', prorettype => 'bool',
1051310515
proargtypes => 'anyelement anyrange', prosrc => 'elem_contained_by_range' },
1051410516
{ oid => '3861',
1051510517
proname => 'range_contained_by', prorettype => 'bool',
@@ -10532,6 +10534,12 @@
1053210534
{ oid => '3867',
1053310535
proname => 'range_union', prorettype => 'anyrange',
1053410536
proargtypes => 'anyrange anyrange', prosrc => 'range_union' },
10537+
{ oid => '9998', descr => 'planner support for range_contains_elem',
10538+
proname => 'range_contains_elem_support', prorettype => 'internal',
10539+
proargtypes => 'internal', prosrc => 'range_contains_elem_support' },
10540+
{ oid => '9999', descr => 'planner support for elem_contained_by_range',
10541+
proname => 'elem_contained_by_range_support', prorettype => 'internal',
10542+
proargtypes => 'internal', prosrc => 'elem_contained_by_range_support' },
1053510543
{ oid => '4057',
1053610544
descr => 'the smallest range which includes both of the given ranges',
1053710545
proname => 'range_merge', prorettype => 'anyrange',

src/include/utils/typcache.h

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ typedef struct TypeCacheEntry
9696
* btree comparison function.
9797
*/
9898
struct TypeCacheEntry *rngelemtype; /* range's element type */
99+
Oid rng_opfamily; /* opfamily to use for range comparisons */
99100
Oid rng_collation; /* collation for comparisons, if any */
100101
FmgrInfo rng_cmp_proc_finfo; /* comparison function */
101102
FmgrInfo rng_canonical_finfo; /* canonicalization function, if any */

0 commit comments

Comments
 (0)