Skip to content

Commit 44ef2da

Browse files
Improve gapfill row count estimate (#9735)
Fixes #9716: bad Gapfill row count estimate of 0 (due to 0 original rows which ignores all the gap fills generating a lot of rows) leading to bad plans and bad performance. It's not a bug fix but an enhancement. We can estimate Gapfill row count much better if we can evaluate start/finish/period at planning time and use more accurate row count estimate of `(subplan rows) + (finish-start)/period` instead of just `subplan rows`. We may have maximum of original rows plus filled gaps in addition to those rows, and this is the estimate used here.
1 parent c5bd8a7 commit 44ef2da

11 files changed

Lines changed: 1947 additions & 62 deletions

File tree

.unreleased/pr_9735

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implements: #9735 Improve GapFill row count estimate
2+
Thanks: @Fabian-2596 for suggesting more accurate GapFill row count estimate

tsl/src/nodes/gapfill/gapfill.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <postgres.h>
99
#include <nodes/pathnodes.h>
1010
#include <nodes/primnodes.h>
11+
#include <utils/builtins.h>
1112

1213
#define GAPFILL_FUNCTION "time_bucket_gapfill"
1314
#define GAPFILL_LOCF_FUNCTION "locf"
@@ -34,3 +35,29 @@ typedef struct GapFillPath
3435
CustomPath cpath;
3536
FuncExpr *func; /* time_bucket_gapfill function call */
3637
} GapFillPath;
38+
39+
typedef enum GapFillBoundary
40+
{
41+
GAPFILL_START,
42+
GAPFILL_END,
43+
} GapFillBoundary;
44+
45+
typedef struct GapFillArgEvalContext
46+
{
47+
PlannerInfo *root; /* needed for evaluation at planning time */
48+
CustomScanState *state; /* needed for evaluation at execution time */
49+
Oid typid; /* gapfill function type */
50+
List *args; /* gapfill function arguments */
51+
FromExpr *jt; /* needed for inferring boundaries from WHERE */
52+
bool estimate_failed; /* whether estimation during planning failed */
53+
} GapFillArgEvalContext;
54+
55+
extern int64 infer_gapfill_boundary(GapFillArgEvalContext *gapfill_ctx, GapFillBoundary boundary);
56+
extern Datum gapfill_estimate_arg(GapFillArgEvalContext *gapfill_plan_ctx, Node *arg, bool *isnull);
57+
58+
extern bool gapfill_is_const_null(Expr *expr);
59+
extern bool gapfill_is_simple_expr(Expr *node);
60+
extern Const *make_const_value_for_gapfill_internal(Oid typid, int64 value);
61+
extern int64 gapfill_datum_get_internal(Datum, Oid);
62+
extern int64 gapfill_bucket_width_get_internal(Oid timetype, Oid argtype, Datum arg,
63+
Interval **interval);

tsl/src/nodes/gapfill/gapfill_exec.c

Lines changed: 108 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,14 @@
2828
#include <utils/typcache.h>
2929

3030
#include <compat/compat.h>
31+
#include "estimate.h"
3132
#include "gapfill.h"
3233
#include "gapfill_internal.h"
3334
#include "interpolate.h"
3435
#include "locf.h"
3536
#include "time_bucket.h"
3637
#include <annotations.h>
3738

38-
typedef enum GapFillBoundary
39-
{
40-
GAPFILL_START,
41-
GAPFILL_END,
42-
} GapFillBoundary;
43-
4439
typedef union GapFillColumnStateUnion
4540
{
4641
GapFillColumnState *base;
@@ -175,8 +170,8 @@ get_timezone_arg(GapFillState *state)
175170
return lthird(state->args);
176171
}
177172

178-
static inline int64
179-
gapfill_period_get_internal(Oid timetype, Oid argtype, Datum arg, Interval **interval)
173+
int64
174+
gapfill_bucket_width_get_internal(Oid timetype, Oid argtype, Datum arg, Interval **interval)
180175
{
181176
switch (timetype)
182177
{
@@ -240,8 +235,8 @@ gapfill_state_create(CustomScan *cscan)
240235
return (Node *) state;
241236
}
242237

243-
static bool
244-
is_const_null(Expr *expr)
238+
bool
239+
gapfill_is_const_null(Expr *expr)
245240
{
246241
return IsA(expr, Const) && castNode(Const, expr)->constisnull;
247242
}
@@ -288,7 +283,7 @@ var_equal(Var *v1, Var *v2)
288283
}
289284

290285
static bool
291-
is_simple_expr_walker(Node *node, void *context)
286+
gapfill_is_simple_expr_walker(Node *node, void *context)
292287
{
293288
if (node == NULL)
294289
{
@@ -328,22 +323,22 @@ is_simple_expr_walker(Node *node, void *context)
328323
default:
329324
return true;
330325
}
331-
return expression_tree_walker(node, is_simple_expr_walker, context);
326+
return expression_tree_walker(node, gapfill_is_simple_expr_walker, context);
332327
}
333328

334329
/*
335330
* check if expression is simple expression and contains only simple
336331
* subexpressions
337332
*/
338-
static bool
339-
is_simple_expr(Expr *node)
333+
bool
334+
gapfill_is_simple_expr(Expr *node)
340335
{
341336
/*
342337
* since expression_tree_walker does early exit on true and we use that to
343338
* skip processing on first non-simple expression we invert return value
344339
* from expression_tree_walker here
345340
*/
346-
return !is_simple_expr_walker((Node *) node, NULL);
341+
return !gapfill_is_simple_expr_walker((Node *) node, NULL);
347342
}
348343

349344
/*
@@ -360,7 +355,7 @@ align_with_time_bucket(GapFillState *state, Expr *expr)
360355
Datum value;
361356
bool isnull;
362357

363-
if (!is_simple_expr(expr))
358+
if (!gapfill_is_simple_expr(expr))
364359
{
365360
ereport(ERROR,
366361
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -399,39 +394,40 @@ align_with_time_bucket(GapFillState *state, Expr *expr)
399394
return gapfill_datum_get_internal(value, state->gapfill_typid);
400395
}
401396

402-
static int64
403-
get_boundary_expr_value(GapFillState *state, GapFillBoundary boundary, Expr *expr)
397+
/* Can be called either with PlannerInfo to estimate boundary value at planning time
398+
* or with GapFillState to evaluate boundary value at execution time.
399+
*/
400+
static Datum
401+
get_boundary_expr_value(GapFillArgEvalContext *gapfill_ctx, GapFillBoundary boundary, Expr *expr,
402+
bool *isnull)
404403
{
405-
Datum arg_value;
406-
bool isnull;
407-
404+
*isnull = false;
405+
Datum arg_value = 0;
408406
/*
409407
* add an explicit cast here if types do not match
410408
*/
411-
if (exprType((Node *) expr) != state->gapfill_typid)
409+
Oid gapfill_typid = gapfill_ctx->typid;
410+
if (exprType((Node *) expr) != gapfill_typid)
412411
{
413-
Oid cast_oid = get_cast_func(exprType((Node *) expr), state->gapfill_typid);
414-
415-
expr = (Expr *) makeFuncExpr(cast_oid,
416-
state->gapfill_typid,
417-
list_make1(expr),
418-
InvalidOid,
419-
InvalidOid,
420-
0);
421-
}
412+
Oid cast_oid = get_cast_func(exprType((Node *) expr), gapfill_typid);
422413

423-
arg_value = gapfill_exec_expr(state, state->scanslot, expr, &isnull);
414+
expr = (Expr *)
415+
makeFuncExpr(cast_oid, gapfill_typid, list_make1(expr), InvalidOid, InvalidOid, 0);
416+
}
424417

425-
if (isnull)
418+
if (gapfill_ctx->state)
426419
{
427-
ereport(ERROR,
428-
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
429-
errmsg("invalid time_bucket_gapfill argument: %s cannot be NULL",
430-
boundary == GAPFILL_START ? "start" : "finish"),
431-
errhint("Specify start and finish as arguments or in the WHERE clause.")));
420+
GapFillState *state = (GapFillState *) gapfill_ctx->state;
421+
arg_value = gapfill_exec_expr(state, state->scanslot, expr, isnull);
422+
}
423+
else
424+
{
425+
/* Estimating start/end boundary at planning time:
426+
* bail out with no error if cannot estimate. */
427+
arg_value = gapfill_estimate_arg(gapfill_ctx, (Node *) expr, isnull);
432428
}
433429

434-
return gapfill_datum_get_internal(arg_value, state->gapfill_typid);
430+
return arg_value;
435431
}
436432

437433
typedef struct CollectBoundaryContext
@@ -543,15 +539,20 @@ collect_boundary_expressions(Node *node, Var *ts_var)
543539
return context.quals;
544540
}
545541

546-
static int64
547-
infer_gapfill_boundary(GapFillState *state, GapFillBoundary boundary)
542+
/* Generic method to collect WHERE quals and obtain suitable start/end gapfill boundaries.
543+
* Can be called from the planner with PlannerInfo to estimate boundaries
544+
* or with GapFillState to evaluate boundaries at execution time.
545+
*/
546+
int64
547+
infer_gapfill_boundary(GapFillArgEvalContext *gapfill_ctx, GapFillBoundary boundary)
548548
{
549-
CustomScan *cscan = castNode(CustomScan, state->csstate.ss.ps.plan);
550-
FuncExpr *func = list_nth(cscan->custom_private, GFP_GapfillFunc);
551-
FromExpr *jt = list_nth(cscan->custom_private, GFP_JoinTree);
549+
Oid gapfill_typid = gapfill_ctx->typid;
550+
List *args = gapfill_ctx->args;
551+
FromExpr *jt = gapfill_ctx->jt;
552+
552553
ListCell *lc;
553554
Var *ts_var;
554-
TypeCacheEntry *tce = lookup_type_cache(state->gapfill_typid, TYPECACHE_BTREE_OPFAMILY);
555+
TypeCacheEntry *tce = lookup_type_cache(gapfill_typid, TYPECACHE_BTREE_OPFAMILY);
555556
int strategy;
556557
Oid lefttype, righttype;
557558
List *quals;
@@ -563,7 +564,7 @@ infer_gapfill_boundary(GapFillState *state, GapFillBoundary boundary)
563564
* if the second argument to time_bucket_gapfill is not a column reference
564565
* we cannot match WHERE clause to the time column
565566
*/
566-
if (!IsA(lsecond(func->args), Var))
567+
if (!IsA(lsecond(args), Var))
567568
{
568569
ereport(ERROR,
569570
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -572,7 +573,7 @@ infer_gapfill_boundary(GapFillState *state, GapFillBoundary boundary)
572573
errhint("Specify start and finish as arguments or in the WHERE clause.")));
573574
}
574575

575-
ts_var = castNode(Var, lsecond(func->args));
576+
ts_var = castNode(Var, lsecond(args));
576577

577578
quals = collect_boundary_expressions((Node *) jt, ts_var);
578579

@@ -613,7 +614,7 @@ infer_gapfill_boundary(GapFillState *state, GapFillBoundary boundary)
613614
* at this stage and Vars will not work either because we execute in
614615
* separate execution context
615616
*/
616-
if (!is_simple_expr(expr) || !var_equal(ts_var, var))
617+
if (!gapfill_is_simple_expr(expr) || !var_equal(ts_var, var))
617618
{
618619
continue;
619620
}
@@ -631,7 +632,35 @@ infer_gapfill_boundary(GapFillState *state, GapFillBoundary boundary)
631632
continue;
632633
}
633634

634-
value = get_boundary_expr_value(state, boundary, expr);
635+
bool isnull;
636+
Datum arg_value = get_boundary_expr_value(gapfill_ctx, boundary, expr, &isnull);
637+
/* If boundary is NULL bail out on the rest of the quals */
638+
if (isnull)
639+
{
640+
/* bail out if during execution */
641+
if (gapfill_ctx->state)
642+
{
643+
ereport(ERROR,
644+
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
645+
errmsg("invalid time_bucket_gapfill argument: %s cannot be NULL",
646+
boundary == GAPFILL_START ? "start" : "finish"),
647+
errhint("Specify start and finish as arguments or in the WHERE clause.")));
648+
}
649+
/* don't estimate the rest of the boundaries but allow planning */
650+
else
651+
{
652+
Assert(gapfill_ctx->root && gapfill_ctx->estimate_failed);
653+
return INVALID_ESTIMATE;
654+
}
655+
}
656+
/* Cannot estimate this boundary at planning time: reset and try the next one */
657+
if (gapfill_ctx->root && gapfill_ctx->estimate_failed)
658+
{
659+
gapfill_ctx->estimate_failed = false;
660+
continue;
661+
}
662+
663+
value = gapfill_datum_get_internal(arg_value, gapfill_ctx->typid);
635664

636665
/*
637666
* if the boundary expression operator does not match the operator
@@ -669,6 +698,15 @@ infer_gapfill_boundary(GapFillState *state, GapFillBoundary boundary)
669698
return boundary_value;
670699
}
671700

701+
/* Could not estimate any boundaries at planning time */
702+
if (gapfill_ctx->root)
703+
{
704+
gapfill_ctx->estimate_failed = true;
705+
return INVALID_ESTIMATE;
706+
}
707+
708+
/* Cannot evaluate boundaries at execution time: bail out */
709+
Assert(gapfill_ctx->state);
672710
ereport(ERROR,
673711
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
674712
errmsg("missing time_bucket_gapfill argument: could not infer %s from WHERE clause",
@@ -677,7 +715,7 @@ infer_gapfill_boundary(GapFillState *state, GapFillBoundary boundary)
677715
pg_unreachable();
678716
}
679717

680-
static Const *
718+
Const *
681719
make_const_value_for_gapfill_internal(Oid typid, int64 value)
682720
{
683721
TypeCacheEntry *tce = lookup_type_cache(typid, 0);
@@ -807,7 +845,7 @@ gapfill_initialize_arguments(GapFillState *state)
807845
Datum arg_value;
808846

809847
/* bucket_width */
810-
if (!is_simple_expr(linitial(state->args)))
848+
if (!gapfill_is_simple_expr(linitial(state->args)))
811849
{
812850
ereport(ERROR,
813851
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -823,10 +861,10 @@ gapfill_initialize_arguments(GapFillState *state)
823861
errmsg("invalid time_bucket_gapfill argument: bucket_width cannot be NULL")));
824862
}
825863

826-
state->gapfill_period = gapfill_period_get_internal(state->gapfill_typid,
827-
exprType(linitial(state->args)),
828-
arg_value,
829-
&state->gapfill_interval);
864+
state->gapfill_period = gapfill_bucket_width_get_internal(state->gapfill_typid,
865+
exprType(linitial(state->args)),
866+
arg_value,
867+
&state->gapfill_interval);
830868

831869
/*
832870
* this would error when trying to align start and stop to bucket_width as well below
@@ -844,9 +882,19 @@ gapfill_initialize_arguments(GapFillState *state)
844882
* check if gapfill start was left out so we have to infer from WHERE
845883
* clause
846884
*/
847-
if (is_const_null(get_start_arg(state)))
885+
CustomScan *cscan = castNode(CustomScan, state->csstate.ss.ps.plan);
886+
GapFillArgEvalContext gapfill_exec_ctx = {
887+
.root = NULL,
888+
.state = (CustomScanState *) state,
889+
.typid = state->gapfill_typid,
890+
.args = state->args,
891+
.jt = list_nth(cscan->custom_private, GFP_JoinTree),
892+
.estimate_failed = false,
893+
};
894+
895+
if (gapfill_is_const_null(get_start_arg(state)))
848896
{
849-
int64 start = infer_gapfill_boundary(state, GAPFILL_START);
897+
int64 start = infer_gapfill_boundary(&gapfill_exec_ctx, GAPFILL_START);
850898
Const *expr = make_const_value_for_gapfill_internal(state->gapfill_typid, start);
851899

852900
state->gapfill_start = align_with_time_bucket(state, (Expr *) expr);
@@ -863,13 +911,13 @@ gapfill_initialize_arguments(GapFillState *state)
863911
state->next_offset = state->gapfill_interval;
864912

865913
/* gap fill end */
866-
if (is_const_null(get_finish_arg(state)))
914+
if (gapfill_is_const_null(get_finish_arg(state)))
867915
{
868-
state->gapfill_end = infer_gapfill_boundary(state, GAPFILL_END);
916+
state->gapfill_end = infer_gapfill_boundary(&gapfill_exec_ctx, GAPFILL_END);
869917
}
870918
else
871919
{
872-
if (!is_simple_expr(get_finish_arg(state)))
920+
if (!gapfill_is_simple_expr(get_finish_arg(state)))
873921
{
874922
ereport(ERROR,
875923
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),

tsl/src/nodes/gapfill/gapfill_internal.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,3 @@ typedef struct GapFillState
126126
Node *gapfill_state_create(CustomScan *);
127127
Expr *gapfill_adjust_varnos(GapFillState *state, Expr *expr);
128128
Datum gapfill_exec_expr(GapFillState *state, TupleTableSlot *, Expr *expr, bool *isnull);
129-
int64 gapfill_datum_get_internal(Datum, Oid);

0 commit comments

Comments
 (0)