public inbox for [email protected]help / color / mirror / Atom feed
[PATCH] Demote "incomplete startup packet" to DEBUG1 3+ messages / 3 participants [nested] [flat]
* [PATCH] Demote "incomplete startup packet" to DEBUG1 @ 2019-02-28 15:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: Christoph Berg @ 2019-02-28 15:12 UTC (permalink / raw) --- src/backend/postmaster/postmaster.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index ccea231e98..2dfb7b6e0a 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -1907,7 +1907,7 @@ ProcessStartupPacket(Port *port, bool SSLdone) * don't clutter the log with a complaint. */ if (!SSLdone) - ereport(COMMERROR, + ereport(DEBUG1, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("incomplete startup packet"))); return STATUS_ERROR; @@ -1919,7 +1919,7 @@ ProcessStartupPacket(Port *port, bool SSLdone) if (len < (int32) sizeof(ProtocolVersion) || len > MAX_STARTUP_PACKET_LENGTH) { - ereport(COMMERROR, + ereport(DEBUG1, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("invalid length of startup packet"))); return STATUS_ERROR; @@ -1938,7 +1938,7 @@ ProcessStartupPacket(Port *port, bool SSLdone) if (pq_getbytes(buf, len) == EOF) { - ereport(COMMERROR, + ereport(DEBUG1, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("incomplete startup packet"))); return STATUS_ERROR; -- 2.20.1 --EeQfGwPcQSOJBaQU-- ^ permalink raw reply [nested|flat] 3+ messages in thread
* [PATCH v7 4/7] Row pattern recognition patch (executor). @ 2023-09-22 04:53 Tatsuo Ishii <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: Tatsuo Ishii @ 2023-09-22 04:53 UTC (permalink / raw) --- src/backend/executor/nodeWindowAgg.c | 853 ++++++++++++++++++++++++++- src/backend/utils/adt/windowfuncs.c | 37 +- src/include/catalog/pg_proc.dat | 6 + src/include/nodes/execnodes.h | 26 + 4 files changed, 909 insertions(+), 13 deletions(-) diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c index 310ac23e3a..84d1b8acaa 100644 --- a/src/backend/executor/nodeWindowAgg.c +++ b/src/backend/executor/nodeWindowAgg.c @@ -36,6 +36,7 @@ #include "access/htup_details.h" #include "catalog/objectaccess.h" #include "catalog/pg_aggregate.h" +#include "catalog/pg_collation_d.h" #include "catalog/pg_proc.h" #include "executor/executor.h" #include "executor/nodeWindowAgg.h" @@ -48,6 +49,7 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/fmgroids.h" #include "utils/expandeddatum.h" #include "utils/lsyscache.h" #include "utils/memutils.h" @@ -182,8 +184,9 @@ static void begin_partition(WindowAggState *winstate); static void spool_tuples(WindowAggState *winstate, int64 pos); static void release_partition(WindowAggState *winstate); -static int row_is_in_frame(WindowAggState *winstate, int64 pos, +static int row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot); + static void update_frameheadpos(WindowAggState *winstate); static void update_frametailpos(WindowAggState *winstate); static void update_grouptailpos(WindowAggState *winstate); @@ -195,9 +198,32 @@ static Datum GetAggInitVal(Datum textInitVal, Oid transtype); static bool are_peers(WindowAggState *winstate, TupleTableSlot *slot1, TupleTableSlot *slot2); -static bool window_gettupleslot(WindowObject winobj, int64 pos, - TupleTableSlot *slot); +static int WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot, + int relpos, int seektype, bool set_mark, + bool *isnull, bool *isout); +static bool window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot); + +static void attno_map(Node *node); +static bool attno_map_walker(Node *node, void *context); +static int row_is_in_reduced_frame(WindowObject winobj, int64 pos); +static bool rpr_is_defined(WindowAggState *winstate); + +static void create_reduced_frame_map(WindowAggState *winstate); +static int get_reduced_frame_map(WindowAggState *winstate, int64 pos); +static void register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val); +static void clear_reduced_frame_map(WindowAggState *winstate); +static void update_reduced_frame(WindowObject winobj, int64 pos); + +static int64 evaluate_pattern(WindowObject winobj, int64 current_pos, + char *vname, StringInfo encoded_str, bool *result); + +static bool get_slots(WindowObject winobj, int64 current_pos); + +static int search_str_set(char *pattern, StringInfo *str_set, int set_size); +static void search_str_set_recurse(char *pattern, StringInfo *str_set, int set_size, int set_index, + int str_index, char *encoded_str, int *resultlen); +static char pattern_initial(WindowAggState *winstate, char *vname); /* * initialize_windowaggregate @@ -673,6 +699,7 @@ eval_windowaggregates(WindowAggState *winstate) WindowObject agg_winobj; TupleTableSlot *agg_row_slot; TupleTableSlot *temp_slot; + bool agg_result_isnull; numaggs = winstate->numaggs; if (numaggs == 0) @@ -778,6 +805,9 @@ eval_windowaggregates(WindowAggState *winstate) * Note that we don't strictly need to restart in the last case, but if * we're going to remove all rows from the aggregation anyway, a restart * surely is faster. + * + * - if RPR is enabled and skip mode is SKIP TO NEXT ROW, + * we restart aggregation too. *---------- */ numaggs_restart = 0; @@ -788,8 +818,11 @@ eval_windowaggregates(WindowAggState *winstate) (winstate->aggregatedbase != winstate->frameheadpos && !OidIsValid(peraggstate->invtransfn_oid)) || (winstate->frameOptions & FRAMEOPTION_EXCLUSION) || - winstate->aggregatedupto <= winstate->frameheadpos) + winstate->aggregatedupto <= winstate->frameheadpos || + (rpr_is_defined(winstate) && + winstate->rpSkipTo == ST_NEXT_ROW)) { + elog(DEBUG1, "peraggstate->restart is set"); peraggstate->restart = true; numaggs_restart++; } @@ -861,8 +894,10 @@ eval_windowaggregates(WindowAggState *winstate) * If we created a mark pointer for aggregates, keep it pushed up to frame * head, so that tuplestore can discard unnecessary rows. */ +#ifdef NOT_USED if (agg_winobj->markptr >= 0) WinSetMarkPosition(agg_winobj, winstate->frameheadpos); +#endif /* * Now restart the aggregates that require it. @@ -917,6 +952,29 @@ eval_windowaggregates(WindowAggState *winstate) { winstate->aggregatedupto = winstate->frameheadpos; ExecClearTuple(agg_row_slot); + + /* + * If RPR is defined, we do not use aggregatedupto_nonrestarted. To + * avoid assertion failure below, we reset aggregatedupto_nonrestarted + * to frameheadpos. + */ + if (rpr_is_defined(winstate)) + aggregatedupto_nonrestarted = winstate->frameheadpos; + } + + agg_result_isnull = false; + /* RPR is defined? */ + if (rpr_is_defined(winstate)) + { + /* + * If the skip mode is SKIP TO PAST LAST ROW and we already know that + * current row is a skipped row, we don't need to accumulate rows, + * just return NULL. Note that for unamtched row, we need to do + * aggregation so that count(*) shows 0, rather than NULL. + */ + if (winstate->rpSkipTo == ST_PAST_LAST_ROW && + get_reduced_frame_map(winstate, winstate->currentpos) == RF_SKIPPED) + agg_result_isnull = true; } /* @@ -930,6 +988,11 @@ eval_windowaggregates(WindowAggState *winstate) { int ret; + elog(DEBUG1, "===== loop in frame starts: " INT64_FORMAT, winstate->aggregatedupto); + + if (agg_result_isnull) + break; + /* Fetch next row if we didn't already */ if (TupIsNull(agg_row_slot)) { @@ -945,9 +1008,28 @@ eval_windowaggregates(WindowAggState *winstate) ret = row_is_in_frame(winstate, winstate->aggregatedupto, agg_row_slot); if (ret < 0) break; + if (ret == 0) goto next_tuple; + if (rpr_is_defined(winstate)) + { + /* + * If the row status at currentpos is already decided and current + * row status is not decided yet, it means we passed the last + * reduced frame. Time to break the loop. + */ + if (get_reduced_frame_map(winstate, winstate->currentpos) != RF_NOT_DETERMINED && + get_reduced_frame_map(winstate, winstate->aggregatedupto) == RF_NOT_DETERMINED) + break; + /* + * Otherwise we need to calculate the reduced frame. + */ + ret = row_is_in_reduced_frame(winstate->agg_winobj, winstate->aggregatedupto); + if (ret == -1) /* unmatched row */ + break; + } + /* Set tuple context for evaluation of aggregate arguments */ winstate->tmpcontext->ecxt_outertuple = agg_row_slot; @@ -976,6 +1058,7 @@ next_tuple: ExecClearTuple(agg_row_slot); } + /* The frame's end is not supposed to move backwards, ever */ Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto); @@ -996,6 +1079,16 @@ next_tuple: peraggstate, result, isnull); + /* + * RPR is defined and we just return NULL because skip mode is SKIP + * TO PAST LAST ROW and current row is skipped row. + */ + if (agg_result_isnull) + { + *isnull = true; + *result = (Datum) 0; + } + /* * save the result in case next row shares the same frame. * @@ -1090,6 +1183,7 @@ begin_partition(WindowAggState *winstate) winstate->framehead_valid = false; winstate->frametail_valid = false; winstate->grouptail_valid = false; + create_reduced_frame_map(winstate); winstate->spooled_rows = 0; winstate->currentpos = 0; winstate->frameheadpos = 0; @@ -2053,6 +2147,8 @@ ExecWindowAgg(PlanState *pstate) CHECK_FOR_INTERRUPTS(); + elog(DEBUG1, "ExecWindowAgg called. pos: " INT64_FORMAT , winstate->currentpos); + if (winstate->status == WINDOWAGG_DONE) return NULL; @@ -2221,6 +2317,17 @@ ExecWindowAgg(PlanState *pstate) /* don't evaluate the window functions when we're in pass-through mode */ if (winstate->status == WINDOWAGG_RUN) { + /* + * If RPR is defined and skip mode is next row, we need to clear existing + * reduced frame info so that we newly calculate the info starting from + * current row. + */ + if (rpr_is_defined(winstate)) + { + if (winstate->rpSkipTo == ST_NEXT_ROW) + clear_reduced_frame_map(winstate); + } + /* * Evaluate true window functions */ @@ -2388,6 +2495,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) TupleDesc scanDesc; ListCell *l; + TargetEntry *te; + Expr *expr; + /* check for unsupported flags */ Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK))); @@ -2483,6 +2593,16 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) winstate->temp_slot_2 = ExecInitExtraTupleSlot(estate, scanDesc, &TTSOpsMinimalTuple); + winstate->prev_slot = ExecInitExtraTupleSlot(estate, scanDesc, + &TTSOpsMinimalTuple); + + winstate->next_slot = ExecInitExtraTupleSlot(estate, scanDesc, + &TTSOpsMinimalTuple); + + winstate->null_slot = ExecInitExtraTupleSlot(estate, scanDesc, + &TTSOpsMinimalTuple); + winstate->null_slot = ExecStoreAllNullTuple(winstate->null_slot); + /* * create frame head and tail slots only if needed (must create slots in * exactly the same cases that update_frameheadpos and update_frametailpos @@ -2667,6 +2787,39 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) winstate->inRangeAsc = node->inRangeAsc; winstate->inRangeNullsFirst = node->inRangeNullsFirst; + /* Set up SKIP TO type */ + winstate->rpSkipTo = node->rpSkipTo; + /* Set up row pattern recognition PATTERN clause */ + winstate->patternVariableList = node->patternVariable; + winstate->patternRegexpList = node->patternRegexp; + + /* Set up row pattern recognition DEFINE clause */ + winstate->defineInitial = node->defineInitial; + winstate->defineVariableList = NIL; + winstate->defineClauseList = NIL; + if (node->defineClause != NIL) + { + /* + * Tweak arg var of PREV/NEXT so that it refers to scan/inner slot. + */ + foreach(l, node->defineClause) + { + char *name; + ExprState *exps; + + te = lfirst(l); + name = te->resname; + expr = te->expr; + + elog(DEBUG1, "defineVariable name: %s", name); + winstate->defineVariableList = lappend(winstate->defineVariableList, + makeString(pstrdup(name))); + attno_map((Node *)expr); + exps = ExecInitExpr(expr, (PlanState *) winstate); + winstate->defineClauseList = lappend(winstate->defineClauseList, exps); + } + } + winstate->all_first = true; winstate->partition_spooled = false; winstate->more_partitions = false; @@ -2674,6 +2827,57 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) return winstate; } +/* + * Rewrite varno of Var node that is the argument of PREV/NET so that it sees + * scan tuple (PREV) or inner tuple (NEXT). + */ +static void +attno_map(Node *node) +{ + (void) expression_tree_walker(node, attno_map_walker, NULL); +} + +static bool +attno_map_walker(Node *node, void *context) +{ + FuncExpr *func; + int nargs; + Expr *expr; + Var *var; + + if (node == NULL) + return false; + + if (IsA(node, FuncExpr)) + { + func = (FuncExpr *)node; + + if (func->funcid == F_PREV || func->funcid == F_NEXT) + { + /* sanity check */ + nargs = list_length(func->args); + if (list_length(func->args) != 1) + elog(ERROR, "PREV/NEXT must have 1 argument but function %d has %d args", func->funcid, nargs); + + expr = (Expr *) lfirst(list_head(func->args)); + if (!IsA(expr, Var)) + elog(ERROR, "PREV/NEXT's arg is not Var"); /* XXX: is it possible that arg type is Const? */ + var = (Var *)expr; + + if (func->funcid == F_PREV) + /* + * Rewrite varno from OUTER_VAR to regular var no so that the + * var references scan tuple. + */ + var->varno = var->varnosyn; + else + var->varno = INNER_VAR; + elog(DEBUG1, "PREV/NEXT's varno is rewritten to: %d", var->varno); + } + } + return expression_tree_walker(node, attno_map_walker, NULL); +} + /* ----------------- * ExecEndWindowAgg * ----------------- @@ -2691,6 +2895,8 @@ ExecEndWindowAgg(WindowAggState *node) ExecClearTuple(node->agg_row_slot); ExecClearTuple(node->temp_slot_1); ExecClearTuple(node->temp_slot_2); + ExecClearTuple(node->prev_slot); + ExecClearTuple(node->next_slot); if (node->framehead_slot) ExecClearTuple(node->framehead_slot); if (node->frametail_slot) @@ -2740,6 +2946,8 @@ ExecReScanWindowAgg(WindowAggState *node) ExecClearTuple(node->agg_row_slot); ExecClearTuple(node->temp_slot_1); ExecClearTuple(node->temp_slot_2); + ExecClearTuple(node->prev_slot); + ExecClearTuple(node->next_slot); if (node->framehead_slot) ExecClearTuple(node->framehead_slot); if (node->frametail_slot) @@ -3100,7 +3308,7 @@ window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot) return false; if (pos < winobj->markpos) - elog(ERROR, "cannot fetch row before WindowObject's mark position"); + elog(ERROR, "cannot fetch row: " INT64_FORMAT " before WindowObject's mark position: " INT64_FORMAT, pos, winobj->markpos ); oldcontext = MemoryContextSwitchTo(winstate->ss.ps.ps_ExprContext->ecxt_per_query_memory); @@ -3420,14 +3628,54 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, WindowAggState *winstate; ExprContext *econtext; TupleTableSlot *slot; - int64 abs_pos; - int64 mark_pos; Assert(WindowObjectIsValid(winobj)); winstate = winobj->winstate; econtext = winstate->ss.ps.ps_ExprContext; slot = winstate->temp_slot_1; + if (WinGetSlotInFrame(winobj, slot, + relpos, seektype, set_mark, + isnull, isout) == 0) + { + econtext->ecxt_outertuple = slot; + return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), + econtext, isnull); + } + + if (isout) + *isout = true; + *isnull = true; + return (Datum) 0; +} + +/* + * WinGetSlotInFrame + * slot: TupleTableSlot to store the result + * relpos: signed rowcount offset from the seek position + * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL + * set_mark: If the row is found/in frame and set_mark is true, the mark is + * moved to the row as a side-effect. + * isnull: output argument, receives isnull status of result + * isout: output argument, set to indicate whether target row position + * is out of frame (can pass NULL if caller doesn't care about this) + * + * Returns 0 if we successfullt got the slot. false if out of frame. + * (also isout is set) + */ +static int +WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot, + int relpos, int seektype, bool set_mark, + bool *isnull, bool *isout) +{ + WindowAggState *winstate; + int64 abs_pos; + int64 mark_pos; + int num_reduced_frame; + + Assert(WindowObjectIsValid(winobj)); + winstate = winobj->winstate; + switch (seektype) { case WINDOW_SEEK_CURRENT: @@ -3494,11 +3742,21 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, winstate->frameOptions); break; } + num_reduced_frame = row_is_in_reduced_frame(winobj, winstate->frameheadpos); + if (num_reduced_frame < 0) + goto out_of_frame; + else if (num_reduced_frame > 0) + if (relpos >= num_reduced_frame) + goto out_of_frame; break; case WINDOW_SEEK_TAIL: /* rejecting relpos > 0 is easy and simplifies code below */ if (relpos > 0) goto out_of_frame; + + /* RPR cares about frame head pos. Need to call update_frameheadpos */ + update_frameheadpos(winstate); + update_frametailpos(winstate); abs_pos = winstate->frametailpos - 1 + relpos; @@ -3565,6 +3823,12 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, mark_pos = 0; /* keep compiler quiet */ break; } + + num_reduced_frame = row_is_in_reduced_frame(winobj, winstate->frameheadpos + relpos); + if (num_reduced_frame < 0) + goto out_of_frame; + else if (num_reduced_frame > 0) + abs_pos = winstate->frameheadpos + relpos + num_reduced_frame - 1; break; default: elog(ERROR, "unrecognized window seek type: %d", seektype); @@ -3583,15 +3847,13 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, *isout = false; if (set_mark) WinSetMarkPosition(winobj, mark_pos); - econtext->ecxt_outertuple = slot; - return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), - econtext, isnull); + return 0; out_of_frame: if (isout) *isout = true; *isnull = true; - return (Datum) 0; + return -1; } /* @@ -3622,3 +3884,572 @@ WinGetFuncArgCurrent(WindowObject winobj, int argno, bool *isnull) return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), econtext, isnull); } + +/* + * rpr_is_defined + * return true if Row pattern recognition is defined. + */ +static +bool rpr_is_defined(WindowAggState *winstate) +{ + return winstate->patternVariableList != NIL; +} + +/* + * row_is_in_reduced_frame + * Determine whether a row is in the current row's reduced window frame according + * to row pattern matching + * + * The row must has been already determined that it is in a full window frame + * and fetched it into slot. + * + * Returns: + * = 0, RPR is not defined. + * >0, if the row is the first in the reduced frame. Return the number of rows in the reduced frame. + * -1, if the row is unmatched row + * -2, if the row is in the reduced frame but needed to be skipped because of + * AFTER MATCH SKIP PAST LAST ROW + */ +static +int row_is_in_reduced_frame(WindowObject winobj, int64 pos) +{ + WindowAggState *winstate = winobj->winstate; + int state; + int rtn; + + if (!rpr_is_defined(winstate)) + { + /* + * RPR is not defined. Assume that we are always in the the reduced + * window frame. + */ + rtn = 0; + elog(DEBUG1, "row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT, rtn, pos); + return rtn; + } + + state = get_reduced_frame_map(winstate, pos); + + if (state == RF_NOT_DETERMINED) + { + update_frameheadpos(winstate); + update_reduced_frame(winobj, pos); + } + + state = get_reduced_frame_map(winstate, pos); + + switch (state) + { + int64 i; + int num_reduced_rows; + + case RF_FRAME_HEAD: + num_reduced_rows = 1; + for (i = pos + 1; get_reduced_frame_map(winstate,i) == RF_SKIPPED; i++) + num_reduced_rows++; + rtn = num_reduced_rows; + break; + + case RF_SKIPPED: + rtn = -2; + break; + + case RF_UNMATCHED: + rtn = -1; + break; + + default: + elog(ERROR, "Unrecognized state: %d at: " INT64_FORMAT, state, pos); + break; + } + + elog(DEBUG1, "row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT, rtn, pos); + return rtn; +} + +#define REDUCED_FRAME_MAP_INIT_SIZE 1024L + +/* + * Create reduced frame map + */ +static +void create_reduced_frame_map(WindowAggState *winstate) +{ + winstate->reduced_frame_map = + MemoryContextAlloc(winstate->partcontext, REDUCED_FRAME_MAP_INIT_SIZE); + winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE; + clear_reduced_frame_map(winstate); +} + +/* + * Clear reduced frame map + */ +static +void clear_reduced_frame_map(WindowAggState *winstate) +{ + Assert(winstate->reduced_frame_map != NULL); + MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED, + winstate->alloc_sz); +} + +/* + * Get reduced frame map specified by pos + */ +static +int get_reduced_frame_map(WindowAggState *winstate, int64 pos) +{ + Assert(winstate->reduced_frame_map != NULL); + + if (pos < 0 || pos >= winstate->alloc_sz) + elog(ERROR, "wrong pos: " INT64_FORMAT, pos); + + return winstate->reduced_frame_map[pos]; +} + +/* + * Add/replace reduced frame map member at pos. + * If there's no enough space, expand the map. + */ +static +void register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val) +{ + int64 realloc_sz; + + Assert(winstate->reduced_frame_map != NULL); + + if (pos < 0) + elog(ERROR, "wrong pos: " INT64_FORMAT, pos); + + if (pos > winstate->alloc_sz - 1) + { + realloc_sz = winstate->alloc_sz * 2; + + winstate->reduced_frame_map = + repalloc(winstate->reduced_frame_map, realloc_sz); + + MemSet(winstate->reduced_frame_map + winstate->alloc_sz, + RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz); + + winstate->alloc_sz = realloc_sz; + } + + winstate->reduced_frame_map[pos] = val; +} + +/* + * update_reduced_frame + * Update reduced frame info. + */ +static +void update_reduced_frame(WindowObject winobj, int64 pos) +{ + WindowAggState *winstate = winobj->winstate; + ListCell *lc1, *lc2; + bool expression_result; + int num_matched_rows; + int64 original_pos; + bool anymatch; + StringInfo encoded_str; + StringInfo pattern_str = makeStringInfo(); + + /* + * Array of pattern variables evaluted to true. + * Each character corresponds to pattern variable. + * Example: + * str_set[0] = "AB"; + * str_set[1] = "AC"; + * In this case at row 0 A and B are true, and A and C are true in row 1. + */ + #define ENCODED_STR_ARRAY_ALLOC_SIZE 128 + StringInfo *str_set = NULL; + int str_set_index; + int str_set_size; + + /* save original pos */ + original_pos = pos; + + /* + * Loop over until none of pattern matches or encounters end of frame. + */ + for (;;) + { + int64 result_pos = -1; + + /* + * Loop over each PATTERN variable. + */ + anymatch = false; + encoded_str = makeStringInfo(); + + forboth(lc1, winstate->patternVariableList, lc2, winstate->patternRegexpList) + { + char *vname = strVal(lfirst(lc1)); + char *quantifier = strVal(lfirst(lc2)); + + elog(DEBUG1, "pos: " INT64_FORMAT " pattern vname: %s quantifier: %s", pos, vname, quantifier); + + expression_result = false; + + /* evaluate row pattern against current row */ + result_pos = evaluate_pattern(winobj, pos, vname, encoded_str, &expression_result); + if (expression_result) + { + elog(DEBUG1, "expression result is true"); + anymatch = true; + } + + /* + * If out of frame, we are done. + */ + if (result_pos < 0) + break; + } + + if (!anymatch) + { + /* none of patterns matched. */ + break; + } + + /* build encoded string array */ + if (str_set == NULL) + { + str_set_index = 0; + str_set_size = ENCODED_STR_ARRAY_ALLOC_SIZE * sizeof(StringInfo); + str_set = palloc(str_set_size); + } + + str_set[str_set_index++] = encoded_str; + + elog(DEBUG1, "pos: " INT64_FORMAT " str_set_index: %d encoded_str: %s", pos, str_set_index, encoded_str->data); + + if (str_set_index >= str_set_size) + { + str_set_size *= 2; + str_set = repalloc(str_set, str_set_size); + } + + /* move to next row */ + pos++; + + if (result_pos < 0) + { + /* out of frame */ + break; + } + } + + if (str_set == NULL) + { + /* no match found in the first row */ + register_reduced_frame_map(winstate, original_pos, RF_UNMATCHED); + return; + } + + elog(DEBUG2, "pos: " INT64_FORMAT " encoded_str: %s", pos, encoded_str->data); + + /* build regular expression */ + pattern_str = makeStringInfo(); + appendStringInfoChar(pattern_str, '^'); + forboth (lc1, winstate->patternVariableList, lc2, winstate->patternRegexpList) + { + char *vname = strVal(lfirst(lc1)); + char *quantifier = strVal(lfirst(lc2)); + char initial; + + initial = pattern_initial(winstate, vname); + Assert(initial != 0); + appendStringInfoChar(pattern_str, initial); + if (quantifier[0]) + appendStringInfoChar(pattern_str, quantifier[0]); + elog(DEBUG1, "vname: %s initial: %c quantifier: %s", vname, initial, quantifier); + } + + elog(DEBUG2, "pos: " INT64_FORMAT " pattern: %s", pos, pattern_str->data); + + /* look for matching pattern variable sequence */ + num_matched_rows = search_str_set(pattern_str->data, str_set, str_set_index); + /* + * We are at the first row in the reduced frame. Save the number of + * matched rows as the number of rows in the reduced frame. + */ + if (num_matched_rows <= 0) + { + /* no match */ + register_reduced_frame_map(winstate, original_pos, RF_UNMATCHED); + } + else + { + int64 i; + + register_reduced_frame_map(winstate, original_pos, RF_FRAME_HEAD); + + for (i = original_pos + 1; i < original_pos + num_matched_rows; i++) + { + register_reduced_frame_map(winstate, i, RF_SKIPPED); + } + } + + return; +} + +/* + * Perform regex search pattern against encoded string array str_set. + * returns the number of longest matching rows. + * str_set: array of encoded string. Each array element corresponds to each + * row. + * set_size: size of set_str array. + */ +static +int search_str_set(char *pattern, StringInfo *str_set, int set_size) +{ + char *encoded_str = palloc0(set_size+1); + int resultlen = 0; + + search_str_set_recurse(pattern, str_set, set_size, 0, 0, + encoded_str, &resultlen); + elog(DEBUG1, "search_str_set returns %d", resultlen); + return resultlen; +} + +/* + * Workhorse of search_str_set. + * + * Recurse among matched pattern variables in a row. The max recursion depth + * is number of pattern variables matched in a row. + */ +static +void search_str_set_recurse(char *pattern, StringInfo *str_set, + int set_size, int set_index, int str_index, + char *encoded_str, int *resultlen) +{ + for (;;) + { + char c; + + c = str_set[set_index]->data[str_index]; + if (c == '\0') + return; + encoded_str[set_index] = c; + set_index++; + + if (set_index >= set_size) + { + Datum d; + text *res; + char *substr; + + /* + * We first perform pattern matching using regexp_instr, then call + * textregexsubstr to get matched substring to know how long the + * matched string is. That is the number of rows in the reduced window + * frame. The reason why we can't call textregexsubstr in the first + * place is, it errors out if pattern does not match. + */ + if (DatumGetInt32(DirectFunctionCall2Coll(regexp_instr, DEFAULT_COLLATION_OID, + PointerGetDatum(cstring_to_text(encoded_str)), + PointerGetDatum(cstring_to_text(pattern)))) > 0) + { + d = DirectFunctionCall2Coll(textregexsubstr, + DEFAULT_COLLATION_OID, + PointerGetDatum(cstring_to_text(encoded_str)), + PointerGetDatum(cstring_to_text(pattern))); + if (d != 0) + { + int len; + + res = DatumGetTextPP(d); + substr = text_to_cstring(res); + len = strlen(substr); + if (len > *resultlen) + /* remember the longest match */ + *resultlen = len; + } + } + return; + } + else + search_str_set_recurse(pattern, str_set, set_size, set_index, str_index + 1, + encoded_str, resultlen); + } +} + +/* + * Evaluate expression associated with PATTERN variable vname. + * relpos is relative row position in a frame (starting from 0). + * "quantifier" is the quatifier part of the PATTERN regular expression. + * Currently only '+' is allowed. + * result is out paramater representing the expression evaluation result + * is true of false. + * Return values are: + * >=0: the last match absolute row position + * other wise out of frame. + */ +static +int64 evaluate_pattern(WindowObject winobj, int64 current_pos, + char *vname, StringInfo encoded_str, bool *result) +{ + WindowAggState *winstate = winobj->winstate; + ExprContext *econtext = winstate->ss.ps.ps_ExprContext; + ListCell *lc1, *lc2, *lc3; + ExprState *pat; + Datum eval_result; + bool out_of_frame = false; + bool isnull; + + forthree (lc1, winstate->defineVariableList, lc2, winstate->defineClauseList, lc3, winstate->defineInitial) + { + char initial; + char *name = strVal(lfirst(lc1)); + + if (strcmp(vname, name)) + continue; + + initial = *(strVal(lfirst(lc3))); + + /* set expression to evaluate */ + pat = lfirst(lc2); + + /* get current, previous and next tuples */ + if (!get_slots(winobj, current_pos)) + { + out_of_frame = true; + } + else + { + /* evaluate the expression */ + eval_result = ExecEvalExpr(pat, econtext, &isnull); + if (isnull) + { + /* expression is NULL */ + elog(DEBUG1, "expression for %s is NULL at row: " INT64_FORMAT, vname, current_pos); + *result = false; + } + else + { + if (!DatumGetBool(eval_result)) + { + /* expression is false */ + elog(DEBUG1, "expression for %s is false at row: " INT64_FORMAT, vname, current_pos); + *result = false; + } + else + { + /* expression is true */ + elog(DEBUG1, "expression for %s is true at row: " INT64_FORMAT, vname, current_pos); + appendStringInfoChar(encoded_str, initial); + *result = true; + } + } + break; + } + + if (out_of_frame) + { + *result = false; + return -1; + } + } + return current_pos; +} + +/* + * Get current, previous and next tuples. + * Returns false if current row is out of partition/full frame. + */ +static +bool get_slots(WindowObject winobj, int64 current_pos) +{ + WindowAggState *winstate = winobj->winstate; + TupleTableSlot *slot; + int ret; + ExprContext *econtext; + + econtext = winstate->ss.ps.ps_ExprContext; + + /* set up current row tuple slot */ + slot = winstate->temp_slot_1; + if (!window_gettupleslot(winobj, current_pos, slot)) + { + elog(DEBUG1, "current row is out of partition at:" INT64_FORMAT, current_pos); + return false; + + ret = row_is_in_frame(winstate, current_pos, slot); + if (ret <= 0) + { + elog(DEBUG1, "current row is out of frame at: " INT64_FORMAT, current_pos); + return false; + } + } + econtext->ecxt_outertuple = slot; + + /* for PREV */ + if (current_pos > 0) + { + slot = winstate->prev_slot; + if (!window_gettupleslot(winobj, current_pos - 1, slot)) + { + elog(DEBUG1, "previous row is out of partition at: " INT64_FORMAT, current_pos - 1); + econtext->ecxt_scantuple = winstate->null_slot; + } + else + { + ret = row_is_in_frame(winstate, current_pos - 1, slot); + if (ret <= 0) + { + elog(DEBUG1, "previous row is out of frame at: " INT64_FORMAT, current_pos - 1); + econtext->ecxt_scantuple = winstate->null_slot; + } + else + { + econtext->ecxt_scantuple = slot; + } + } + } + else + econtext->ecxt_scantuple = winstate->null_slot; + + /* for NEXT */ + slot = winstate->next_slot; + if (!window_gettupleslot(winobj, current_pos + 1, slot)) + { + elog(DEBUG1, "next row is out of partiton at: " INT64_FORMAT, current_pos + 1); + econtext->ecxt_innertuple = winstate->null_slot; + } + else + { + ret = row_is_in_frame(winstate, current_pos + 1, slot); + if (ret <= 0) + { + elog(DEBUG1, "next row is out of frame at: " INT64_FORMAT, current_pos + 1); + econtext->ecxt_innertuple = winstate->null_slot; + } + else + econtext->ecxt_innertuple = slot; + } + return true; +} + +/* + * Return pattern variable initial character + * matching with pattern variable name vname. + * If not found, return 0. + */ +static +char pattern_initial(WindowAggState *winstate, char *vname) +{ + char initial; + char *name; + ListCell *lc1, *lc2; + + forboth (lc1, winstate->defineVariableList, lc2, winstate->defineInitial) + { + name = strVal(lfirst(lc1)); /* DEFINE variable name */ + initial = *(strVal(lfirst(lc2))); /* DEFINE variable initial */ + + + if (!strcmp(name, vname)) + return initial; /* found */ + } + return 0; +} diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c index b87a624fb2..9ebcc7b5d2 100644 --- a/src/backend/utils/adt/windowfuncs.c +++ b/src/backend/utils/adt/windowfuncs.c @@ -13,6 +13,9 @@ */ #include "postgres.h" +#include "catalog/pg_collation_d.h" +#include "executor/executor.h" +#include "nodes/execnodes.h" #include "nodes/supportnodes.h" #include "utils/builtins.h" #include "windowapi.h" @@ -36,11 +39,19 @@ typedef struct int64 remainder; /* (total rows) % (bucket num) */ } ntile_context; +/* + * rpr process information. + * Used for AFTER MATCH SKIP PAST LAST ROW + */ +typedef struct SkipContext +{ + int64 pos; /* last row absolute position */ +} SkipContext; + static bool rank_up(WindowObject winobj); static Datum leadlag_common(FunctionCallInfo fcinfo, bool forward, bool withoffset, bool withdefault); - /* * utility routine for *_rank functions. */ @@ -673,7 +684,7 @@ window_last_value(PG_FUNCTION_ARGS) bool isnull; result = WinGetFuncArgInFrame(winobj, 0, - 0, WINDOW_SEEK_TAIL, true, + 0, WINDOW_SEEK_TAIL, false, &isnull, NULL); if (isnull) PG_RETURN_NULL(); @@ -713,3 +724,25 @@ window_nth_value(PG_FUNCTION_ARGS) PG_RETURN_DATUM(result); } + +/* + * prev + * Dummy function to invoke RPR's navigation operator "PREV". + * This is *not* a window function. + */ +Datum +window_prev(PG_FUNCTION_ARGS) +{ + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); +} + +/* + * next + * Dummy function to invoke RPR's navigation operation "NEXT". + * This is *not* a window function. + */ +Datum +window_next(PG_FUNCTION_ARGS) +{ + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 9805bc6118..d20f803cf5 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -10416,6 +10416,12 @@ { oid => '3114', descr => 'fetch the Nth row value', proname => 'nth_value', prokind => 'w', prorettype => 'anyelement', proargtypes => 'anyelement int4', prosrc => 'window_nth_value' }, +{ oid => '6122', descr => 'previous value', + proname => 'prev', provolatile => 's', prorettype => 'anyelement', + proargtypes => 'anyelement', prosrc => 'window_prev' }, +{ oid => '6123', descr => 'next value', + proname => 'next', provolatile => 's', prorettype => 'anyelement', + proargtypes => 'anyelement', prosrc => 'window_next' }, # functions for range types { oid => '3832', descr => 'I/O', diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cb714f4a19..63feb68f60 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -2471,6 +2471,11 @@ typedef enum WindowAggStatus * tuples during spool */ } WindowAggStatus; +#define RF_NOT_DETERMINED 0 +#define RF_FRAME_HEAD 1 +#define RF_SKIPPED 2 +#define RF_UNMATCHED 3 + typedef struct WindowAggState { ScanState ss; /* its first field is NodeTag */ @@ -2519,6 +2524,15 @@ typedef struct WindowAggState int64 groupheadpos; /* current row's peer group head position */ int64 grouptailpos; /* " " " " tail position (group end+1) */ + /* these fields are used in Row pattern recognition: */ + RPSkipTo rpSkipTo; /* Row Pattern Skip To type */ + List *patternVariableList; /* list of row pattern variables names (list of String) */ + List *patternRegexpList; /* list of row pattern regular expressions ('+' or ''. list of String) */ + List *defineVariableList; /* list of row pattern definition variables (list of String) */ + List *defineClauseList; /* expression for row pattern definition + * search conditions ExprState list */ + List *defineInitial; /* list of row pattern definition variable initials (list of String) */ + MemoryContext partcontext; /* context for partition-lifespan data */ MemoryContext aggcontext; /* shared context for aggregate working data */ MemoryContext curaggcontext; /* current aggregate's working data */ @@ -2555,6 +2569,18 @@ typedef struct WindowAggState TupleTableSlot *agg_row_slot; TupleTableSlot *temp_slot_1; TupleTableSlot *temp_slot_2; + + /* temporary slots for RPR */ + TupleTableSlot *prev_slot; /* PREV row navigation operator */ + TupleTableSlot *next_slot; /* NEXT row navigation operator */ + TupleTableSlot *null_slot; /* all NULL slot */ + + /* + * Each byte corresponds to a row positioned at absolute its pos in + * partition. See above definition for RF_* + */ + char *reduced_frame_map; + int64 alloc_sz; /* size of the map */ } WindowAggState; /* ---------------- -- 2.25.1 ----Next_Part(Fri_Sep_22_14_16_40_2023_530)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v7-0005-Row-pattern-recognition-patch-docs.patch" ^ permalink raw reply [nested|flat] 3+ messages in thread
* Re: Expand applicability of aggregate's sortop optimization @ 2024-07-19 03:47 Andrei Lepikhov <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: Andrei Lepikhov @ 2024-07-19 03:47 UTC (permalink / raw) To: Matthias van de Meent <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>; David Rowley <[email protected]>; [email protected] On 18/7/2024 19:49, Matthias van de Meent wrote: > On Wed, 17 Jul 2024 at 16:09, Andrei Lepikhov <[email protected]> wrote: >> >> On 17/7/2024 16:33, Matthias van de Meent wrote: >>> On Wed, 17 Jul 2024 at 05:29, Andrei Lepikhov <[email protected]> wrote: > Because the @<@ and @>@ operators are not registered as commutative, > it couldn't apply the optimization in your patch, while the btree > operator check does allow it to apply the optimization. Ok, I got it. And next issue: I think it would be better to save cycles than to free some piece of memory, so why not to break the foreach cycle if you already matched the opfamily? Also, in the patch attached I added your smoothed test to the aggregates.sql > > Aside: Arguably, checking for commutator operators would not be > incorrect when looking at it from "applied operators" point of view, > but if that commutative operator isn't registered as opposite ordering > of the same btree opclass, then we'd probably break some assumptions > of some aggregate's sortop - it could be registered with another > opclass, and that could cause us to select a different btree opclass > (thus: ordering) than is indicated to be required by the aggregate; > the thing we're trying to protect against hereYes, I also think if someone doesn't register < as a commutator to >, it may mean they do it intentionally: may be it is a bit different sortings? - this subject is too far from my experience and I can agree with your approach. -- regards, Andrei Lepikhov diff --git a/src/backend/optimizer/plan/planagg.c b/src/backend/optimizer/plan/planagg.c index afb5445b77..9b1d6f4e43 100644 --- a/src/backend/optimizer/plan/planagg.c +++ b/src/backend/optimizer/plan/planagg.c @@ -253,6 +253,20 @@ can_minmax_aggs(PlannerInfo *root, List **context) if (list_length(aggref->args) != 1) return false; /* it couldn't be MIN/MAX */ + /* + * We might implement the optimization when a FILTER clause is present + * by adding the filter to the quals of the generated subquery. For + * now, just punt. + */ + if (aggref->aggfilter != NULL) + return false; + + curTarget = (TargetEntry *) linitial(aggref->args); + + aggsortop = fetch_agg_sort_op(aggref->aggfnoid); + if (!OidIsValid(aggsortop)) + return false; /* not a MIN/MAX aggregate */ + /* * ORDER BY is usually irrelevant for MIN/MAX, but it can change the * outcome if the aggsortop's operator class recognizes non-identical @@ -267,22 +281,55 @@ can_minmax_aggs(PlannerInfo *root, List **context) * quickly. */ if (aggref->aggorder != NIL) - return false; - /* note: we do not care if DISTINCT is mentioned ... */ - - /* - * We might implement the optimization when a FILTER clause is present - * by adding the filter to the quals of the generated subquery. For - * now, just punt. - */ - if (aggref->aggfilter != NULL) - return false; + { + SortGroupClause *orderClause; + + /* + * If the order clause is the same column as the one we're + * aggregating, we can still use the index: It is undefined which + * value is MIN() or MAX(), as well as which value is first or + * last when sorted. So, we can still use the index IFF the + * aggregated expression equals the expression used in the + * ordering operation. + */ + + /* + * We only accept a single argument to min/max aggregates, + * orderings that have more clauses won't provide correct results. + */ + Assert(list_length(aggref->aggorder) == 1); + + orderClause = castNode(SortGroupClause, linitial(aggref->aggorder)); + + Assert(orderClause->tleSortGroupRef == curTarget->ressortgroupref); + + if (orderClause->sortop != aggsortop) + { + List *btclasses; + ListCell *cell; + bool match = false; + + btclasses = get_op_btree_interpretation(orderClause->sortop); + + foreach(cell, btclasses) + { + OpBtreeInterpretation *interpretation; + interpretation = (OpBtreeInterpretation *) lfirst(cell); + + if (op_in_opfamily(aggsortop, interpretation->opfamily_id)) + { + match = true; + break; + } + } + + if (!match) + return false; + } + } - aggsortop = fetch_agg_sort_op(aggref->aggfnoid); - if (!OidIsValid(aggsortop)) - return false; /* not a MIN/MAX aggregate */ + /* note: we do not care if DISTINCT is mentioned ... */ - curTarget = (TargetEntry *) linitial(aggref->args); if (contain_mutable_functions((Node *) curTarget->expr)) return false; /* not potentially indexable */ diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index a5596ab210..94c8cf77f8 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -1003,6 +1003,124 @@ select max(unique1) from tenk1 where unique1 > 42; 9999 (1 row) +-- When sorting on the column that's being aggregated, indexes can also be +-- used, but only when the aggregate's operator has the same ordering behavior +-- as the ORDER BY-clause, i.e. if it is in the same btree opclass as the one +-- chosen for the ORDER BY clause. +explain (costs off) + select min(unique1 ORDER BY unique1 ASC NULLS LAST) from tenk1; + QUERY PLAN +------------------------------------------------------------ + Result + InitPlan 1 + -> Limit + -> Index Only Scan using tenk1_unique1 on tenk1 + Index Cond: (unique1 IS NOT NULL) +(5 rows) + +select min(unique1 ORDER BY unique1 ASC NULLS LAST) from tenk1; + min +----- + 0 +(1 row) + +explain (costs off) + select max(unique1 ORDER BY unique1 USING <) from tenk1; + QUERY PLAN +--------------------------------------------------------------------- + Result + InitPlan 1 + -> Limit + -> Index Only Scan Backward using tenk1_unique1 on tenk1 + Index Cond: (unique1 IS NOT NULL) +(5 rows) + +select max(unique1 ORDER BY unique1 USING <) from tenk1; + max +------ + 9999 +(1 row) + +explain (costs off) + select max(unique1 ORDER BY tenthous) from tenk1; + QUERY PLAN +------------------------------- + Aggregate + -> Sort + Sort Key: tenthous + -> Seq Scan on tenk1 +(4 rows) + +select max(unique1 ORDER BY tenthous) from tenk1; + max +------ + 9999 +(1 row) + +CREATE OPERATOR @>@ (function=int4gt, leftarg=int4, rightarg=int4); +CREATE OPERATOR @<@ (function=int4lt, leftarg=int4, rightarg=int4); +CREATE OPERATOR @=@ (function=int4eq, leftarg=int4, rightarg=int4); +CREATE OPERATOR FAMILY my_family USING btree; +CREATE OPERATOR CLASS my_int_ops + FOR TYPE int + USING btree FAMILY my_family AS + OPERATOR 1 @<@ FOR SEARCH, + OPERATOR 5 @>@ FOR SEARCH, + OPERATOR 3 @=@, + FUNCTION 1 btint4cmp; +CREATE AGGREGATE my_int_max ( + BASETYPE = int4, + SFUNC = int4larger, + STYPE = int4, + SORTOP = @>@ +); +CREATE INDEX idx_int4 ON tenk1 (unique1 my_int_ops); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT my_int_max(unique1::int4 ORDER BY unique1::int4 USING @<@ ) from tenk1; + QUERY PLAN +----------------------------------------------------------------------- + Result + Output: (InitPlan 1).col1 + InitPlan 1 + -> Limit + Output: tenk1.unique1 + -> Index Only Scan Backward using idx_int4 on public.tenk1 + Output: tenk1.unique1 + Index Cond: (tenk1.unique1 IS NOT NULL) +(8 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT my_int_max(unique1::int4 ORDER BY unique1::int4 USING @>@ ) from tenk1; + QUERY PLAN +----------------------------------------------------------------------- + Result + Output: (InitPlan 1).col1 + InitPlan 1 + -> Limit + Output: tenk1.unique1 + -> Index Only Scan Backward using idx_int4 on public.tenk1 + Output: tenk1.unique1 + Index Cond: (tenk1.unique1 IS NOT NULL) +(8 rows) + +DROP AGGREGATE my_int_max(int4); +DROP OPERATOR CLASS my_int_ops USING btree CASCADE; +NOTICE: drop cascades to index idx_int4 +DROP OPERATOR FAMILY my_family USING btree; +DROP OPERATOR @>@ (int4, int4); +DROP OPERATOR @<@ (int4, int4); +DROP OPERATOR @=@ (int4, int4); +-- But even then, the index can't be used if we order by multiple columns. +explain (costs off) + select max(unique1 ORDER BY unique1, tenthous) from tenk1; + QUERY PLAN +------------------------------------- + Aggregate + -> Sort + Sort Key: unique1, tenthous + -> Seq Scan on tenk1 +(4 rows) + -- the planner may choose a generic aggregate here if parallel query is -- enabled, since that plan will be parallel safe and the "optimized" -- plan, which has almost identical cost, will not be. we want to test diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index ca6d1bcfb7..19729ae5f2 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -368,6 +368,57 @@ explain (costs off) select max(unique1) from tenk1 where unique1 > 42; select max(unique1) from tenk1 where unique1 > 42; +-- When sorting on the column that's being aggregated, indexes can also be +-- used, but only when the aggregate's operator has the same ordering behavior +-- as the ORDER BY-clause, i.e. if it is in the same btree opclass as the one +-- chosen for the ORDER BY clause. +explain (costs off) + select min(unique1 ORDER BY unique1 ASC NULLS LAST) from tenk1; +select min(unique1 ORDER BY unique1 ASC NULLS LAST) from tenk1; +explain (costs off) + select max(unique1 ORDER BY unique1 USING <) from tenk1; +select max(unique1 ORDER BY unique1 USING <) from tenk1; +explain (costs off) + select max(unique1 ORDER BY tenthous) from tenk1; +select max(unique1 ORDER BY tenthous) from tenk1; + +CREATE OPERATOR @>@ (function=int4gt, leftarg=int4, rightarg=int4); +CREATE OPERATOR @<@ (function=int4lt, leftarg=int4, rightarg=int4); +CREATE OPERATOR @=@ (function=int4eq, leftarg=int4, rightarg=int4); + +CREATE OPERATOR FAMILY my_family USING btree; +CREATE OPERATOR CLASS my_int_ops + FOR TYPE int + USING btree FAMILY my_family AS + OPERATOR 1 @<@ FOR SEARCH, + OPERATOR 5 @>@ FOR SEARCH, + OPERATOR 3 @=@, + FUNCTION 1 btint4cmp; + +CREATE AGGREGATE my_int_max ( + BASETYPE = int4, + SFUNC = int4larger, + STYPE = int4, + SORTOP = @>@ +); + +CREATE INDEX idx_int4 ON tenk1 (unique1 my_int_ops); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT my_int_max(unique1::int4 ORDER BY unique1::int4 USING @<@ ) from tenk1; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT my_int_max(unique1::int4 ORDER BY unique1::int4 USING @>@ ) from tenk1; + +DROP AGGREGATE my_int_max(int4); +DROP OPERATOR CLASS my_int_ops USING btree CASCADE; +DROP OPERATOR FAMILY my_family USING btree; +DROP OPERATOR @>@ (int4, int4); +DROP OPERATOR @<@ (int4, int4); +DROP OPERATOR @=@ (int4, int4); + +-- But even then, the index can't be used if we order by multiple columns. +explain (costs off) + select max(unique1 ORDER BY unique1, tenthous) from tenk1; + -- the planner may choose a generic aggregate here if parallel query is -- enabled, since that plan will be parallel safe and the "optimized" -- plan, which has almost identical cost, will not be. we want to test Attachments: [text/plain] rewritten-patch-v4.diff (9.7K, ../../[email protected]/2-rewritten-patch-v4.diff) download | inline diff: diff --git a/src/backend/optimizer/plan/planagg.c b/src/backend/optimizer/plan/planagg.c index afb5445b77..9b1d6f4e43 100644 --- a/src/backend/optimizer/plan/planagg.c +++ b/src/backend/optimizer/plan/planagg.c @@ -253,6 +253,20 @@ can_minmax_aggs(PlannerInfo *root, List **context) if (list_length(aggref->args) != 1) return false; /* it couldn't be MIN/MAX */ + /* + * We might implement the optimization when a FILTER clause is present + * by adding the filter to the quals of the generated subquery. For + * now, just punt. + */ + if (aggref->aggfilter != NULL) + return false; + + curTarget = (TargetEntry *) linitial(aggref->args); + + aggsortop = fetch_agg_sort_op(aggref->aggfnoid); + if (!OidIsValid(aggsortop)) + return false; /* not a MIN/MAX aggregate */ + /* * ORDER BY is usually irrelevant for MIN/MAX, but it can change the * outcome if the aggsortop's operator class recognizes non-identical @@ -267,22 +281,55 @@ can_minmax_aggs(PlannerInfo *root, List **context) * quickly. */ if (aggref->aggorder != NIL) - return false; - /* note: we do not care if DISTINCT is mentioned ... */ - - /* - * We might implement the optimization when a FILTER clause is present - * by adding the filter to the quals of the generated subquery. For - * now, just punt. - */ - if (aggref->aggfilter != NULL) - return false; + { + SortGroupClause *orderClause; + + /* + * If the order clause is the same column as the one we're + * aggregating, we can still use the index: It is undefined which + * value is MIN() or MAX(), as well as which value is first or + * last when sorted. So, we can still use the index IFF the + * aggregated expression equals the expression used in the + * ordering operation. + */ + + /* + * We only accept a single argument to min/max aggregates, + * orderings that have more clauses won't provide correct results. + */ + Assert(list_length(aggref->aggorder) == 1); + + orderClause = castNode(SortGroupClause, linitial(aggref->aggorder)); + + Assert(orderClause->tleSortGroupRef == curTarget->ressortgroupref); + + if (orderClause->sortop != aggsortop) + { + List *btclasses; + ListCell *cell; + bool match = false; + + btclasses = get_op_btree_interpretation(orderClause->sortop); + + foreach(cell, btclasses) + { + OpBtreeInterpretation *interpretation; + interpretation = (OpBtreeInterpretation *) lfirst(cell); + + if (op_in_opfamily(aggsortop, interpretation->opfamily_id)) + { + match = true; + break; + } + } + + if (!match) + return false; + } + } - aggsortop = fetch_agg_sort_op(aggref->aggfnoid); - if (!OidIsValid(aggsortop)) - return false; /* not a MIN/MAX aggregate */ + /* note: we do not care if DISTINCT is mentioned ... */ - curTarget = (TargetEntry *) linitial(aggref->args); if (contain_mutable_functions((Node *) curTarget->expr)) return false; /* not potentially indexable */ diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index a5596ab210..94c8cf77f8 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -1003,6 +1003,124 @@ select max(unique1) from tenk1 where unique1 > 42; 9999 (1 row) +-- When sorting on the column that's being aggregated, indexes can also be +-- used, but only when the aggregate's operator has the same ordering behavior +-- as the ORDER BY-clause, i.e. if it is in the same btree opclass as the one +-- chosen for the ORDER BY clause. +explain (costs off) + select min(unique1 ORDER BY unique1 ASC NULLS LAST) from tenk1; + QUERY PLAN +------------------------------------------------------------ + Result + InitPlan 1 + -> Limit + -> Index Only Scan using tenk1_unique1 on tenk1 + Index Cond: (unique1 IS NOT NULL) +(5 rows) + +select min(unique1 ORDER BY unique1 ASC NULLS LAST) from tenk1; + min +----- + 0 +(1 row) + +explain (costs off) + select max(unique1 ORDER BY unique1 USING <) from tenk1; + QUERY PLAN +--------------------------------------------------------------------- + Result + InitPlan 1 + -> Limit + -> Index Only Scan Backward using tenk1_unique1 on tenk1 + Index Cond: (unique1 IS NOT NULL) +(5 rows) + +select max(unique1 ORDER BY unique1 USING <) from tenk1; + max +------ + 9999 +(1 row) + +explain (costs off) + select max(unique1 ORDER BY tenthous) from tenk1; + QUERY PLAN +------------------------------- + Aggregate + -> Sort + Sort Key: tenthous + -> Seq Scan on tenk1 +(4 rows) + +select max(unique1 ORDER BY tenthous) from tenk1; + max +------ + 9999 +(1 row) + +CREATE OPERATOR @>@ (function=int4gt, leftarg=int4, rightarg=int4); +CREATE OPERATOR @<@ (function=int4lt, leftarg=int4, rightarg=int4); +CREATE OPERATOR @=@ (function=int4eq, leftarg=int4, rightarg=int4); +CREATE OPERATOR FAMILY my_family USING btree; +CREATE OPERATOR CLASS my_int_ops + FOR TYPE int + USING btree FAMILY my_family AS + OPERATOR 1 @<@ FOR SEARCH, + OPERATOR 5 @>@ FOR SEARCH, + OPERATOR 3 @=@, + FUNCTION 1 btint4cmp; +CREATE AGGREGATE my_int_max ( + BASETYPE = int4, + SFUNC = int4larger, + STYPE = int4, + SORTOP = @>@ +); +CREATE INDEX idx_int4 ON tenk1 (unique1 my_int_ops); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT my_int_max(unique1::int4 ORDER BY unique1::int4 USING @<@ ) from tenk1; + QUERY PLAN +----------------------------------------------------------------------- + Result + Output: (InitPlan 1).col1 + InitPlan 1 + -> Limit + Output: tenk1.unique1 + -> Index Only Scan Backward using idx_int4 on public.tenk1 + Output: tenk1.unique1 + Index Cond: (tenk1.unique1 IS NOT NULL) +(8 rows) + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT my_int_max(unique1::int4 ORDER BY unique1::int4 USING @>@ ) from tenk1; + QUERY PLAN +----------------------------------------------------------------------- + Result + Output: (InitPlan 1).col1 + InitPlan 1 + -> Limit + Output: tenk1.unique1 + -> Index Only Scan Backward using idx_int4 on public.tenk1 + Output: tenk1.unique1 + Index Cond: (tenk1.unique1 IS NOT NULL) +(8 rows) + +DROP AGGREGATE my_int_max(int4); +DROP OPERATOR CLASS my_int_ops USING btree CASCADE; +NOTICE: drop cascades to index idx_int4 +DROP OPERATOR FAMILY my_family USING btree; +DROP OPERATOR @>@ (int4, int4); +DROP OPERATOR @<@ (int4, int4); +DROP OPERATOR @=@ (int4, int4); +-- But even then, the index can't be used if we order by multiple columns. +explain (costs off) + select max(unique1 ORDER BY unique1, tenthous) from tenk1; + QUERY PLAN +------------------------------------- + Aggregate + -> Sort + Sort Key: unique1, tenthous + -> Seq Scan on tenk1 +(4 rows) + -- the planner may choose a generic aggregate here if parallel query is -- enabled, since that plan will be parallel safe and the "optimized" -- plan, which has almost identical cost, will not be. we want to test diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index ca6d1bcfb7..19729ae5f2 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -368,6 +368,57 @@ explain (costs off) select max(unique1) from tenk1 where unique1 > 42; select max(unique1) from tenk1 where unique1 > 42; +-- When sorting on the column that's being aggregated, indexes can also be +-- used, but only when the aggregate's operator has the same ordering behavior +-- as the ORDER BY-clause, i.e. if it is in the same btree opclass as the one +-- chosen for the ORDER BY clause. +explain (costs off) + select min(unique1 ORDER BY unique1 ASC NULLS LAST) from tenk1; +select min(unique1 ORDER BY unique1 ASC NULLS LAST) from tenk1; +explain (costs off) + select max(unique1 ORDER BY unique1 USING <) from tenk1; +select max(unique1 ORDER BY unique1 USING <) from tenk1; +explain (costs off) + select max(unique1 ORDER BY tenthous) from tenk1; +select max(unique1 ORDER BY tenthous) from tenk1; + +CREATE OPERATOR @>@ (function=int4gt, leftarg=int4, rightarg=int4); +CREATE OPERATOR @<@ (function=int4lt, leftarg=int4, rightarg=int4); +CREATE OPERATOR @=@ (function=int4eq, leftarg=int4, rightarg=int4); + +CREATE OPERATOR FAMILY my_family USING btree; +CREATE OPERATOR CLASS my_int_ops + FOR TYPE int + USING btree FAMILY my_family AS + OPERATOR 1 @<@ FOR SEARCH, + OPERATOR 5 @>@ FOR SEARCH, + OPERATOR 3 @=@, + FUNCTION 1 btint4cmp; + +CREATE AGGREGATE my_int_max ( + BASETYPE = int4, + SFUNC = int4larger, + STYPE = int4, + SORTOP = @>@ +); + +CREATE INDEX idx_int4 ON tenk1 (unique1 my_int_ops); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT my_int_max(unique1::int4 ORDER BY unique1::int4 USING @<@ ) from tenk1; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT my_int_max(unique1::int4 ORDER BY unique1::int4 USING @>@ ) from tenk1; + +DROP AGGREGATE my_int_max(int4); +DROP OPERATOR CLASS my_int_ops USING btree CASCADE; +DROP OPERATOR FAMILY my_family USING btree; +DROP OPERATOR @>@ (int4, int4); +DROP OPERATOR @<@ (int4, int4); +DROP OPERATOR @=@ (int4, int4); + +-- But even then, the index can't be used if we order by multiple columns. +explain (costs off) + select max(unique1 ORDER BY unique1, tenthous) from tenk1; + -- the planner may choose a generic aggregate here if parallel query is -- enabled, since that plan will be parallel safe and the "optimized" -- plan, which has almost identical cost, will not be. we want to test ^ permalink raw reply [nested|flat] 3+ messages in thread
end of thread, other threads:[~2024-07-19 03:47 UTC | newest] Thread overview: 3+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2019-02-28 15:12 [PATCH] Demote "incomplete startup packet" to DEBUG1 Christoph Berg <[email protected]> 2023-09-22 04:53 [PATCH v7 4/7] Row pattern recognition patch (executor). Tatsuo Ishii <[email protected]> 2024-07-19 03:47 Re: Expand applicability of aggregate's sortop optimization Andrei Lepikhov <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox