Hi
> PERCENT and WITH TIES can play together, per spec. > I also Implement PERCENT WITH TIES option. patch is attached i don't start a new tread because the patches share common code regards Surafel
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 9fc53cad68..d512f4ddd0 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -3097,7 +3097,8 @@ estimate_path_cost_size(PlannerInfo *root, if (fpextra && fpextra->has_limit) { adjust_limit_rows_costs(&rows, &startup_cost, &total_cost, - fpextra->offset_est, fpextra->count_est); + LIMIT_OPTION_COUNT, fpextra->offset_est, + fpextra->count_est); retrieved_rows = rows; } } diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml index b93e4ca208..9811380ec6 100644 --- a/doc/src/sgml/ref/select.sgml +++ b/doc/src/sgml/ref/select.sgml @@ -44,7 +44,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replaceable class="parameter">expression</replac [ ORDER BY <replaceable class="parameter">expression</replaceable> [ ASC | DESC | USING <replaceable class="parameter">operator</replaceable> ] [ NULLS { FIRST | LAST } ] [, ...] ] [ LIMIT { <replaceable class="parameter">count</replaceable> | ALL } ] [ OFFSET <replaceable class="parameter">start</replaceable> [ ROW | ROWS ] ] - [ FETCH { FIRST | NEXT } [ <replaceable class="parameter">count</replaceable> ] { ROW | ROWS } { ONLY | WITH TIES } ] + [ FETCH { FIRST | NEXT } [ <replaceable class="parameter">count</replaceable> ] [ PERCENT ] { ROW | ROWS } { ONLY | WITH TIES } ] [ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } [ OF <replaceable class="parameter">table_name</replaceable> [, ...] ] [ NOWAIT | SKIP LOCKED ] [...] ] <phrase>where <replaceable class="parameter">from_item</replaceable> can be one of:</phrase> @@ -1434,7 +1434,7 @@ OFFSET <replaceable class="parameter">start</replaceable> which <productname>PostgreSQL</productname> also supports. It is: <synopsis> OFFSET <replaceable class="parameter">start</replaceable> { ROW | ROWS } -FETCH { FIRST | NEXT } [ <replaceable class="parameter">count</replaceable> ] { ROW | ROWS } { ONLY | WITH TIES } +FETCH { FIRST | NEXT } [ <replaceable class="parameter">count</replaceable> ] [ PERCENT ] { ROW | ROWS } { ONLY | WITH TIES } </synopsis> In this syntax, the <replaceable class="parameter">start</replaceable> or <replaceable class="parameter">count</replaceable> value is required by @@ -1444,6 +1444,9 @@ FETCH { FIRST | NEXT } [ <replaceable class="parameter">count</replaceable> ] { ambiguity. If <replaceable class="parameter">count</replaceable> is omitted in a <literal>FETCH</literal> clause, it defaults to 1. + With <literal>PERCENT</literal> count specifies the maximum number of rows to return + in percentage round up to the nearest integer. Using <literal>PERCENT</literal> + is best suited to returning single-digit percentages of the query's total row count. The <literal>WITH TIES</literal> option is used to return any additional rows that tie for the last place in the result set according to the <literal>ORDER BY</literal> clause; <literal>ORDER BY</literal> diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index b6e58e8493..68e0c5e2e1 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -344,7 +344,7 @@ F862 <result offset clause> in subqueries YES F863 Nested <result offset clause> in <query expression> YES F864 Top-level <result offset clause> in views YES F865 <offset row count> in <result offset clause> YES -F866 FETCH FIRST clause: PERCENT option NO +F866 FETCH FIRST clause: PERCENT option YES F867 FETCH FIRST clause: WITH TIES option YES R010 Row pattern recognition: FROM clause NO R020 Row pattern recognition: WINDOW clause NO diff --git a/src/backend/executor/nodeLimit.c b/src/backend/executor/nodeLimit.c index d85cf7d93e..cfb28a9fd4 100644 --- a/src/backend/executor/nodeLimit.c +++ b/src/backend/executor/nodeLimit.c @@ -21,6 +21,8 @@ #include "postgres.h" +#include <math.h> + #include "executor/executor.h" #include "executor/nodeLimit.h" #include "miscadmin.h" @@ -29,6 +31,8 @@ static void recompute_limits(LimitState *node); static int64 compute_tuples_needed(LimitState *node); +#define IsPercentOption(opt) \ + (opt == LIMIT_OPTION_PERCENT || opt == LIMIT_OPTION_PER_WITH_TIES) /* ---------------------------------------------------------------- * ExecLimit @@ -53,6 +57,7 @@ ExecLimit(PlanState *pstate) */ direction = node->ps.state->es_direction; outerPlan = outerPlanState(node); + slot = node->subSlot; /* * The main logic is a simple state machine. @@ -82,7 +87,15 @@ ExecLimit(PlanState *pstate) /* * Check for empty window; if so, treat like empty subplan. */ - if (node->count <= 0 && !node->noCount) + if (IsPercentOption(node->limitOption)) + { + if (node->percent == 0.0) + { + node->lstate = LIMIT_EMPTY; + return NULL; + } + } + else if (node->count <= 0 && !node->noCount) { node->lstate = LIMIT_EMPTY; return NULL; @@ -118,6 +131,16 @@ ExecLimit(PlanState *pstate) break; } + /* + * We may needed this tuple in subsequent scan so put it into + * tuplestore. + */ + if (IsPercentOption(node->limitOption)) + { + tuplestore_puttupleslot(node->tupleStore, slot); + tuplestore_advance(node->tupleStore, true); + } + /* * Okay, we have the first tuple of the window. */ @@ -135,6 +158,85 @@ ExecLimit(PlanState *pstate) case LIMIT_INWINDOW: if (ScanDirectionIsForward(direction)) { + /* + * In case of coming back from backward scan the tuple is + * already in tuple store. + */ + if (IsPercentOption(node->limitOption) && node->backwardPosition > 0) + { + if (tuplestore_gettupleslot_heaptuple(node->tupleStore, true, true, slot)) + { + node->subSlot = slot; + node->position++; + node->backwardPosition--; + return slot; + } + else + { + node->lstate = LIMIT_SUBPLANEOF; + return NULL; + } + } + + /* + * In LIMIT_OPTION_PERCENT case no need of executing outerPlan + * multiple times. + */ + if (IsPercentOption(node->limitOption) && node->reachEnd) + { + node->lstate = LIMIT_WINDOWEND; + + /* + * If we know we won't need to back up, we can release + * resources at this point. + */ + if (!(node->ps.state->es_top_eflags & EXEC_FLAG_BACKWARD)) + (void) ExecShutdownNode(outerPlan); + + return NULL; + } + + /* + * When in percentage mode, we need to see if we can get any + * additional rows from the subplan (enough to increase the + * node->count value). + */ + if (IsPercentOption(node->limitOption)) + { + /* + * loop until the node->count became greater than the + * number of tuple returned so far + */ + do + { + int64 cnt; + + slot = ExecProcNode(outerPlan); + if (TupIsNull(slot)) + { + node->reachEnd = true; + node->lstate = LIMIT_SUBPLANEOF; + + /* + * The only operation from here is backward scan + * but there's no API to refetch the tuple at the + * current position. We have to move one tuple + * backward, and then we will scan forward for it + * for the first tuple and precede as usual for + * the rest + */ + tuplestore_advance(node->tupleStore, false); + return NULL; + } + + tuplestore_puttupleslot(node->tupleStore, slot); + + cnt = tuplestore_tuple_count(node->tupleStore) + node->offset; + + node->count = ceil(node->percent * cnt / 100.0); + } while (node->position - node->offset >= node->count); + } + /* * Forwards scan, so check for stepping off end of window. At * the end of the window, the behavior depends on whether WITH @@ -152,7 +254,8 @@ ExecLimit(PlanState *pstate) * whether rescans are possible. */ if (!node->noCount && - node->position - node->offset >= node->count) + node->position - node->offset >= node->count + && !IsPercentOption(node->limitOption)) { if (node->limitOption == LIMIT_OPTION_COUNT) { @@ -166,54 +269,88 @@ ExecLimit(PlanState *pstate) } } else + { + if (IsPercentOption(node->limitOption)) + { + if (tuplestore_gettupleslot_heaptuple(node->tupleStore, true, true, slot)) + { + node->subSlot = slot; + node->position++; + break; + } + else + { + node->lstate = LIMIT_SUBPLANEOF; + return NULL; + } + + } + else + { + /* + * Get next tuple from subplan, if any. + */ + slot = ExecProcNode(outerPlan); + if (TupIsNull(slot)) + { + node->lstate = LIMIT_SUBPLANEOF; + return NULL; + } + + /* + * If WITH TIES is active, and this is the last + * in-window tuple, save it to be used in subsequent + * WINDOWEND_TIES processing. + */ + if (node->limitOption == LIMIT_OPTION_WITH_TIES && + node->position - node->offset == node->count - 1) + { + ExecCopySlot(node->last_slot, slot); + } + node->subSlot = slot; + node->position++; + break; + } + } + } + else + { + /* + * Backwards scan, so check for stepping off start of window. + * As above, only change state-machine status if so. + */ + if (node->position <= node->offset + 1) + { + node->lstate = LIMIT_WINDOWSTART; + return NULL; + } + + /* In percent case the result is already in tuplestore */ + if (IsPercentOption(node->limitOption)) + { + if (tuplestore_gettupleslot_heaptuple(node->tupleStore, false, true, slot)) + { + node->subSlot = slot; + node->position--; + node->backwardPosition++; + break; + } + else + elog(ERROR, "LIMIT subplan failed to run backwards"); + } + else { /* - * Get next tuple from subplan, if any. + * Get previous tuple from subplan; there should be one! */ slot = ExecProcNode(outerPlan); if (TupIsNull(slot)) - { - node->lstate = LIMIT_SUBPLANEOF; - return NULL; - } - - /* - * If WITH TIES is active, and this is the last in-window - * tuple, save it to be used in subsequent WINDOWEND_TIES - * processing. - */ - if (node->limitOption == LIMIT_OPTION_WITH_TIES && - node->position - node->offset == node->count - 1) - { - ExecCopySlot(node->last_slot, slot); - } + elog(ERROR, "LIMIT subplan failed to run backwards"); node->subSlot = slot; - node->position++; + node->position--; break; } } - else - { - /* - * Backwards scan, so check for stepping off start of window. - * As above, only change state-machine status if so. - */ - if (node->position <= node->offset + 1) - { - node->lstate = LIMIT_WINDOWSTART; - return NULL; - } - - /* - * Get previous tuple from subplan; there should be one! - */ - slot = ExecProcNode(outerPlan); - if (TupIsNull(slot)) - elog(ERROR, "LIMIT subplan failed to run backwards"); - node->subSlot = slot; - node->position--; - break; - } Assert(node->lstate == LIMIT_WINDOWEND_TIES); /* FALL THRU */ @@ -279,15 +416,32 @@ ExecLimit(PlanState *pstate) return NULL; /* - * Backing up from subplan EOF, so re-fetch previous tuple; there - * should be one! Note previous tuple must be in window. + * Scan forward for the first tuple */ - slot = ExecProcNode(outerPlan); - if (TupIsNull(slot)) - elog(ERROR, "LIMIT subplan failed to run backwards"); - node->subSlot = slot; - node->lstate = LIMIT_INWINDOW; - /* position does not change 'cause we didn't advance it before */ + if (IsPercentOption(node->limitOption)) + { + if (tuplestore_gettupleslot_heaptuple(node->tupleStore, true, true, slot)) + { + node->subSlot = slot; + node->lstate = LIMIT_INWINDOW; + } + else + elog(ERROR, "LIMIT subplan failed to run backwards"); + } + else + { + /* + * Backing up from subplan EOF, so re-fetch previous tuple; + * there should be one! Note previous tuple must be in + * window. + */ + slot = ExecProcNode(outerPlan); + if (TupIsNull(slot)) + elog(ERROR, "LIMIT subplan failed to run backwards"); + node->subSlot = slot; + node->lstate = LIMIT_INWINDOW; + /* position does not change 'cause we didn't advance it before */ + } break; case LIMIT_WINDOWEND: @@ -393,12 +547,36 @@ recompute_limits(LimitState *node) } else { - node->count = DatumGetInt64(val); - if (node->count < 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_ROW_COUNT_IN_LIMIT_CLAUSE), - errmsg("LIMIT must not be negative"))); - node->noCount = false; + if (IsPercentOption(node->limitOption)) + { + /* + * We expect to return at least one row (unless there are no + * rows in the subplan), and we'll update this count later as + * we go. + */ + node->count = 0; + node->percent = DatumGetFloat8(val); + + if (node->percent < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_ROW_COUNT_IN_LIMIT_CLAUSE), + errmsg("PERCENT must not be negative"))); + + if (node->percent > 100) + ereport(ERROR, + (errcode(ERRCODE_INVALID_ROW_COUNT_IN_LIMIT_CLAUSE), + errmsg("PERCENT must not be greater than 100"))); + + } + else + { + node->count = DatumGetInt64(val); + if (node->count < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_ROW_COUNT_IN_LIMIT_CLAUSE), + errmsg("LIMIT must not be negative"))); + + } } } else @@ -411,6 +589,8 @@ recompute_limits(LimitState *node) /* Reset position to start-of-scan */ node->position = 0; node->subSlot = NULL; + node->reachEnd = false; + node->backwardPosition = 0; /* Set state-machine state */ node->lstate = LIMIT_RESCAN; @@ -419,7 +599,8 @@ recompute_limits(LimitState *node) * Notify child node about limit. Note: think not to "optimize" by * skipping ExecSetTupleBound if compute_tuples_needed returns < 0. We * must update the child node anyway, in case this is a rescan and the - * previous time we got a different result. + * previous time we got a different result. In LIMIT_OPTION_PERCENT option + * there are no bound on the number of output tuples */ ExecSetTupleBound(compute_tuples_needed(node), outerPlanState(node)); } @@ -431,7 +612,8 @@ recompute_limits(LimitState *node) static int64 compute_tuples_needed(LimitState *node) { - if ((node->noCount) || (node->limitOption == LIMIT_OPTION_WITH_TIES)) + if ((node->noCount) || (node->limitOption == LIMIT_OPTION_WITH_TIES) + || (IsPercentOption(node->limitOption))) return -1; /* Note: if this overflows, we'll return a negative value, which is OK */ return node->count + node->offset; @@ -521,6 +703,9 @@ ExecInitLimit(Limit *node, EState *estate, int eflags) &limitstate->ps); } + if (IsPercentOption(node->limitOption)) + limitstate->tupleStore = tuplestore_begin_heap(true, false, work_mem); + return limitstate; } @@ -536,6 +721,8 @@ ExecEndLimit(LimitState *node) { ExecFreeExprContext(&node->ps); ExecEndNode(outerPlanState(node)); + if (node->tupleStore != NULL) + tuplestore_end(node->tupleStore); } @@ -555,4 +742,6 @@ ExecReScanLimit(LimitState *node) */ if (node->ps.lefttree->chgParam == NULL) ExecReScan(node->ps.lefttree); + if (node->tupleStore != NULL) + tuplestore_clear(node->tupleStore); } diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index c1fc866cbf..c4dabb02de 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -3665,7 +3665,7 @@ create_limit_path(PlannerInfo *root, RelOptInfo *rel, adjust_limit_rows_costs(&pathnode->path.rows, &pathnode->path.startup_cost, &pathnode->path.total_cost, - offset_est, count_est); + limitOption, offset_est, count_est); return pathnode; } @@ -3690,6 +3690,7 @@ void adjust_limit_rows_costs(double *rows, /* in/out parameter */ Cost *startup_cost, /* in/out parameter */ Cost *total_cost, /* in/out parameter */ + LimitOption limitOption, int64 offset_est, int64 count_est) { diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index dbb47d4982..0168cb37f8 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -680,7 +680,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY + PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PERCENT PLACING PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -11559,6 +11559,14 @@ limit_clause: n->limitOption = LIMIT_OPTION_COUNT; $$ = n; } + | FETCH first_or_next select_fetch_first_value PERCENT row_or_rows ONLY + { + SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); + n->limitOffset = NULL; + n->limitCount = $3; + n->limitOption = LIMIT_OPTION_PERCENT; + $$ = n; + } | FETCH first_or_next select_fetch_first_value row_or_rows WITH TIES { SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); @@ -15202,6 +15210,7 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PERCENT | PLANS | POLICY | PRECEDING diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 6fff13479e..d3782697c8 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -1754,8 +1754,10 @@ transformLimitClause(ParseState *pstate, Node *clause, return NULL; qual = transformExpr(pstate, clause, exprKind); - - qual = coerce_to_specific_type(pstate, qual, INT8OID, constructName); + if (limitOption == LIMIT_OPTION_PERCENT && strcmp(constructName, "LIMIT") == 0) + qual = coerce_to_specific_type(pstate, qual, FLOAT8OID, constructName); + else + qual = coerce_to_specific_type(pstate, qual, INT8OID, constructName); /* LIMIT can't refer to any variables of the current query */ checkExprIsVarFree(pstate, qual, constructName); diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2cbcb4b85e..43e5647201 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -5243,7 +5243,15 @@ get_select_query_def(Query *query, deparse_context *context, -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); get_rule_expr(query->limitOffset, context, false); } - if (query->limitCount != NULL) + if (query->limitOption == LIMIT_OPTION_PERCENT) + { + appendContextKeyword(context, " FETCH FIRST ", + -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); + get_rule_expr(query->limitCount, context, false); + appendContextKeyword(context, " PERCENT ROWS ONLY ", + -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); + } + if (query->limitCount != NULL && query->limitOption != LIMIT_OPTION_PERCENT) { if (query->limitOption == LIMIT_OPTION_WITH_TIES) { diff --git a/src/backend/utils/sort/tuplestore.c b/src/backend/utils/sort/tuplestore.c index 452a85a423..8743a0f03d 100644 --- a/src/backend/utils/sort/tuplestore.c +++ b/src/backend/utils/sort/tuplestore.c @@ -1100,6 +1100,39 @@ tuplestore_gettupleslot(Tuplestorestate *state, bool forward, } } +/* + * tuplestore_gettupleslot_heaptuple + * It is similar to tuplestore_gettupleslot except it return stored HeapTuple + * instead of MinimalTuple + */ +bool +tuplestore_gettupleslot_heaptuple(Tuplestorestate *state, bool forward, + bool copy, TupleTableSlot *slot) +{ + MinimalTuple tuple; + HeapTuple htuple; + bool should_free; + + tuple = (MinimalTuple) tuplestore_gettuple(state, forward, &should_free); + + if (tuple) + { + if (copy && !should_free) + { + tuple = heap_copy_minimal_tuple(tuple); + should_free = true; + } + htuple = heap_tuple_from_minimal_tuple(tuple); + ExecForceStoreHeapTuple(htuple, slot, should_free); + return true; + } + else + { + ExecClearTuple(slot); + return false; + } +} + /* * tuplestore_advance - exported function to adjust position without fetching * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index cf832d7f90..d9c441187a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -2501,6 +2501,11 @@ typedef struct LimitState LimitOption limitOption; /* limit specification type */ int64 offset; /* current OFFSET value */ int64 count; /* current COUNT, if any */ + float8 percent; /* percentage */ + int64 backwardPosition; /* the number of tuple returned in + * backward scan */ + bool reachEnd; /* if true, outerPlan executed until the end */ + Tuplestorestate *tupleStore; /* holds the returned tuple */ bool noCount; /* if true, ignore count */ LimitStateCond lstate; /* state machine status, as above */ int64 position; /* 1-based index of last tuple returned */ diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 381d84b4e4..9253ac734c 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -836,6 +836,8 @@ typedef enum LimitOption { LIMIT_OPTION_COUNT, /* FETCH FIRST... ONLY */ LIMIT_OPTION_WITH_TIES, /* FETCH FIRST... WITH TIES */ + LIMIT_OPTION_PERCENT, /* FETCH FIRST... PERCENT ONLY */ + LIMIT_OPTION_PER_WITH_TIES, /* FETCH FIRST... PERCENT WITH TIES */ LIMIT_OPTION_DEFAULT, /* No limit present */ } LimitOption; diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index 715a24ad29..ddc8f74138 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -272,7 +272,7 @@ extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel, int64 offset_est, int64 count_est); extern void adjust_limit_rows_costs(double *rows, Cost *startup_cost, Cost *total_cost, - int64 offset_est, int64 count_est); + LimitOption limitOption, int64 offset_est, int64 count_est); extern Path *reparameterize_path(PlannerInfo *root, Path *path, Relids required_outer, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 08f22ce211..1605a6f8ea 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -306,6 +306,7 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD) +PG_KEYWORD("percent", PERCENT, UNRESERVED_KEYWORD) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD) diff --git a/src/include/utils/tuplestore.h b/src/include/utils/tuplestore.h index 2c6403f9e8..1d59aa0d57 100644 --- a/src/include/utils/tuplestore.h +++ b/src/include/utils/tuplestore.h @@ -47,7 +47,6 @@ typedef struct Tuplestorestate Tuplestorestate; extern Tuplestorestate *tuplestore_begin_heap(bool randomAccess, bool interXact, int maxKBytes); - extern void tuplestore_set_eflags(Tuplestorestate *state, int eflags); extern void tuplestore_puttupleslot(Tuplestorestate *state, @@ -72,9 +71,12 @@ extern bool tuplestore_in_memory(Tuplestorestate *state); extern bool tuplestore_gettupleslot(Tuplestorestate *state, bool forward, bool copy, TupleTableSlot *slot); +extern bool tuplestore_gettupleslot_heaptuple(Tuplestorestate *state, bool forward, + bool copy, TupleTableSlot *slot); extern bool tuplestore_advance(Tuplestorestate *state, bool forward); + extern bool tuplestore_skiptuples(Tuplestorestate *state, int64 ntuples, bool forward); diff --git a/src/test/regress/expected/limit.out b/src/test/regress/expected/limit.out index e6f6809fbe..46557e2dae 100644 --- a/src/test/regress/expected/limit.out +++ b/src/test/regress/expected/limit.out @@ -108,6 +108,63 @@ SELECT ''::text AS five, unique1, unique2, stringu1 | 904 | 793 | UIAAAA (5 rows) +-- +-- PERCENT +-- Check the PERCENT option of limit clause +-- +SELECT ''::text AS two, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 50 + ORDER BY unique1 FETCH FIRST 1 PERCENT ROWS ONLY; + two | unique1 | unique2 | stringu1 +-----+---------+---------+---------- + | 51 | 76 | ZBAAAA + | 52 | 985 | ACAAAA + | 53 | 196 | BCAAAA + | 54 | 356 | CCAAAA + | 55 | 627 | DCAAAA + | 56 | 54 | ECAAAA + | 57 | 942 | FCAAAA + | 58 | 114 | GCAAAA + | 59 | 593 | HCAAAA + | 60 | 483 | ICAAAA +(10 rows) + +SELECT ''::text AS two, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 60 AND unique1 < 63 + ORDER BY unique1 FETCH FIRST 50 PERCENT ROWS ONLY; + two | unique1 | unique2 | stringu1 +-----+---------+---------+---------- + | 61 | 560 | JCAAAA +(1 row) + +SELECT ''::text AS three, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 100 + ORDER BY unique1 FETCH FIRST 1 PERCENT ROWS ONLY OFFSET 20; + three | unique1 | unique2 | stringu1 +-------+---------+---------+---------- + | 121 | 700 | REAAAA + | 122 | 519 | SEAAAA + | 123 | 777 | TEAAAA + | 124 | 503 | UEAAAA + | 125 | 849 | VEAAAA + | 126 | 330 | WEAAAA + | 127 | 511 | XEAAAA + | 128 | 721 | YEAAAA + | 129 | 696 | ZEAAAA +(9 rows) + +SELECT ''::text AS eleven, unique1, unique2, stringu1 + FROM onek WHERE unique1 < 50 + ORDER BY unique1 DESC FETCH FIRST 10 PERCENT ROWS ONLY OFFSET 39; + eleven | unique1 | unique2 | stringu1 +--------+---------+---------+---------- + | 10 | 520 | KAAAAA + | 9 | 49 | JAAAAA + | 8 | 653 | IAAAAA + | 7 | 647 | HAAAAA + | 6 | 978 | GAAAAA +(5 rows) + -- Test null limit and offset. The planner would discard a simple null -- constant, so to ensure executor is exercised, do this: select * from int8_tbl limit (case when random() < 0.5 then null::bigint end); @@ -338,6 +395,46 @@ fetch backward all in c5; 123 | 456 (2 rows) +declare c6 cursor for select * from int8_tbl fetch first 50 percent rows only; +fetch all in c6; + q1 | q2 +------------------+------------------ + 123 | 456 + 123 | 4567890123456789 + 4567890123456789 | 123 +(3 rows) + +fetch 1 in c6; + q1 | q2 +----+---- +(0 rows) + +fetch backward 1 in c6; + q1 | q2 +------------------+----- + 4567890123456789 | 123 +(1 row) + +fetch backward all in c6; + q1 | q2 +-----+------------------ + 123 | 4567890123456789 + 123 | 456 +(2 rows) + +fetch backward 1 in c6; + q1 | q2 +----+---- +(0 rows) + +fetch all in c6; + q1 | q2 +------------------+------------------ + 123 | 456 + 123 | 4567890123456789 + 4567890123456789 | 123 +(3 rows) + rollback; -- Stress test for variable LIMIT in conjunction with bounded-heap sorting SELECT diff --git a/src/test/regress/sql/limit.sql b/src/test/regress/sql/limit.sql index d2d4ef132d..c6e660913f 100644 --- a/src/test/regress/sql/limit.sql +++ b/src/test/regress/sql/limit.sql @@ -31,6 +31,23 @@ SELECT ''::text AS five, unique1, unique2, stringu1 FROM onek ORDER BY unique1 LIMIT 5 OFFSET 900; +-- +-- PERCENT +-- Check the PERCENT option of limit clause +-- +SELECT ''::text AS two, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 50 + ORDER BY unique1 FETCH FIRST 1 PERCENT ROWS ONLY; +SELECT ''::text AS two, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 60 AND unique1 < 63 + ORDER BY unique1 FETCH FIRST 50 PERCENT ROWS ONLY; +SELECT ''::text AS three, unique1, unique2, stringu1 + FROM onek WHERE unique1 > 100 + ORDER BY unique1 FETCH FIRST 1 PERCENT ROWS ONLY OFFSET 20; +SELECT ''::text AS eleven, unique1, unique2, stringu1 + FROM onek WHERE unique1 < 50 + ORDER BY unique1 DESC FETCH FIRST 10 PERCENT ROWS ONLY OFFSET 39; + -- Test null limit and offset. The planner would discard a simple null -- constant, so to ensure executor is exercised, do this: select * from int8_tbl limit (case when random() < 0.5 then null::bigint end); @@ -38,7 +55,6 @@ select * from int8_tbl offset (case when random() < 0.5 then null::bigint end); -- Test assorted cases involving backwards fetch from a LIMIT plan node begin; - declare c1 cursor for select * from int8_tbl limit 10; fetch all in c1; fetch 1 in c1; @@ -81,6 +97,14 @@ fetch backward all in c5; fetch all in c5; fetch backward all in c5; +declare c6 cursor for select * from int8_tbl fetch first 50 percent rows only; +fetch all in c6; +fetch 1 in c6; +fetch backward 1 in c6; +fetch backward all in c6; +fetch backward 1 in c6; +fetch all in c6; + rollback; -- Stress test for variable LIMIT in conjunction with bounded-heap sorting
diff --git a/src/backend/executor/nodeLimit.c b/src/backend/executor/nodeLimit.c index cfb28a9fd4..bb117c77fd 100644 --- a/src/backend/executor/nodeLimit.c +++ b/src/backend/executor/nodeLimit.c @@ -193,6 +193,12 @@ ExecLimit(PlanState *pstate) if (!(node->ps.state->es_top_eflags & EXEC_FLAG_BACKWARD)) (void) ExecShutdownNode(outerPlan); + /* + * The only operation from here is backward scan We have + * to move one postion forward to get previous tuple + */ + tuplestore_advance(node->tupleStore, true); + return NULL; } @@ -215,26 +221,51 @@ ExecLimit(PlanState *pstate) if (TupIsNull(slot)) { node->reachEnd = true; - node->lstate = LIMIT_SUBPLANEOF; - /* - * The only operation from here is backward scan - * but there's no API to refetch the tuple at the - * current position. We have to move one tuple - * backward, and then we will scan forward for it - * for the first tuple and precede as usual for - * the rest + * If WITH TIES is active, get previous tuple, + * save it to be used in subsequent WINDOWEND_TIES + * processing. */ - tuplestore_advance(node->tupleStore, false); - return NULL; - } + if (node->limitOption == LIMIT_OPTION_PER_WITH_TIES) + { + slot = node->subSlot; + tuplestore_advance(node->tupleStore, false); + if (!tuplestore_gettupleslot_heaptuple(node->tupleStore, true, true, slot)) + { + node->lstate = LIMIT_SUBPLANEOF; + tuplestore_advance(node->tupleStore, true); + return NULL; + } + + ExecCopySlot(node->last_slot, slot); + node->lstate = LIMIT_WINDOWEND_TIES; + /* we'll fall through to the next case */ + } + else + { + node->lstate = LIMIT_SUBPLANEOF; - tuplestore_puttupleslot(node->tupleStore, slot); + /* + * The only operation from here is backward + * scan but there's no API to refetch the + * tuple at the current position. We have to + * move one postion backward, and then we will + * scan forward for it for the first tuple and + * precede as usual for the rest + */ + tuplestore_advance(node->tupleStore, true); + return NULL; + } + } + if (node->lstate != LIMIT_WINDOWEND_TIES) + { + tuplestore_puttupleslot(node->tupleStore, slot); - cnt = tuplestore_tuple_count(node->tupleStore) + node->offset; + cnt = tuplestore_tuple_count(node->tupleStore) + node->offset; - node->count = ceil(node->percent * cnt / 100.0); - } while (node->position - node->offset >= node->count); + node->count = ceil(node->percent * cnt / 100.0); + } + } while (node->position - node->offset >= node->count && node->lstate != LIMIT_WINDOWEND_TIES); } /* @@ -255,7 +286,7 @@ ExecLimit(PlanState *pstate) */ if (!node->noCount && node->position - node->offset >= node->count - && !IsPercentOption(node->limitOption)) + && !IsPercentOption(node->limitOption) && node->lstate != LIMIT_WINDOWEND_TIES) { if (node->limitOption == LIMIT_OPTION_COUNT) { @@ -268,49 +299,45 @@ ExecLimit(PlanState *pstate) /* we'll fall through to the next case */ } } - else + else if (IsPercentOption(node->limitOption) && node->lstate != LIMIT_WINDOWEND_TIES) { - if (IsPercentOption(node->limitOption)) + if (tuplestore_gettupleslot_heaptuple(node->tupleStore, true, true, slot)) { - if (tuplestore_gettupleslot_heaptuple(node->tupleStore, true, true, slot)) - { - node->subSlot = slot; - node->position++; - break; - } - else - { - node->lstate = LIMIT_SUBPLANEOF; - return NULL; - } - - } - else - { - /* - * Get next tuple from subplan, if any. - */ - slot = ExecProcNode(outerPlan); - if (TupIsNull(slot)) - { - node->lstate = LIMIT_SUBPLANEOF; - return NULL; - } - - /* - * If WITH TIES is active, and this is the last - * in-window tuple, save it to be used in subsequent - * WINDOWEND_TIES processing. - */ - if (node->limitOption == LIMIT_OPTION_WITH_TIES && - node->position - node->offset == node->count - 1) - { - ExecCopySlot(node->last_slot, slot); - } node->subSlot = slot; node->position++; break; } + else + { + node->lstate = LIMIT_SUBPLANEOF; + return NULL; + } + } + else if (!IsPercentOption(node->limitOption) && node->lstate != LIMIT_WINDOWEND_TIES) + { + /* + * Get next tuple from subplan, if any. + */ + slot = ExecProcNode(outerPlan); + if (TupIsNull(slot)) + { + node->lstate = LIMIT_SUBPLANEOF; + return NULL; + } + + /* + * If WITH TIES is active, and this is the last in-window + * tuple, save it to be used in subsequent WINDOWEND_TIES + * processing. + */ + if (node->limitOption == LIMIT_OPTION_WITH_TIES && + node->position - node->offset == node->count - 1) + { + ExecCopySlot(node->last_slot, slot); + } + node->subSlot = slot; + node->position++; + break; } } else @@ -359,14 +386,25 @@ ExecLimit(PlanState *pstate) if (ScanDirectionIsForward(direction)) { /* - * Advance the subplan until we find the first row with - * different ORDER BY pathkeys. + * Advance the subplan or tuple store until we find the first + * row with different ORDER BY pathkeys. */ - slot = ExecProcNode(outerPlan); - if (TupIsNull(slot)) + if (node->limitOption == LIMIT_OPTION_PER_WITH_TIES) { - node->lstate = LIMIT_SUBPLANEOF; - return NULL; + if (!tuplestore_gettupleslot_heaptuple(node->tupleStore, true, true, slot)) + { + node->lstate = LIMIT_SUBPLANEOF; + return NULL; + } + } + else + { + slot = ExecProcNode(outerPlan); + if (TupIsNull(slot)) + { + node->lstate = LIMIT_SUBPLANEOF; + return NULL; + } } /* @@ -399,15 +437,30 @@ ExecLimit(PlanState *pstate) } /* - * Get previous tuple from subplan; there should be one! And - * change state-machine status. + * Get previous tuple from subplan or tuple store; there + * should be one! And change state-machine status. */ - slot = ExecProcNode(outerPlan); - if (TupIsNull(slot)) - elog(ERROR, "LIMIT subplan failed to run backwards"); - node->subSlot = slot; - node->position--; - node->lstate = LIMIT_INWINDOW; + if (node->limitOption == LIMIT_OPTION_PER_WITH_TIES) + { + if (tuplestore_gettupleslot_heaptuple(node->tupleStore, false, true, slot)) + { + node->backwardPosition++; + node->position--; + node->subSlot = slot; + node->lstate = LIMIT_INWINDOW; + } + else + elog(ERROR, "LIMIT subplan failed to run backwards"); + } + else + { + slot = ExecProcNode(outerPlan); + if (TupIsNull(slot)) + elog(ERROR, "LIMIT subplan failed to run backwards"); + node->subSlot = slot; + node->position--; + node->lstate = LIMIT_INWINDOW; + } } break; @@ -416,11 +469,12 @@ ExecLimit(PlanState *pstate) return NULL; /* - * Scan forward for the first tuple + * Scan forward for the previous tuple. there should be one! Note + * previous tuple must be in window. */ if (IsPercentOption(node->limitOption)) { - if (tuplestore_gettupleslot_heaptuple(node->tupleStore, true, true, slot)) + if (tuplestore_gettupleslot_heaptuple(node->tupleStore, false, true, slot)) { node->subSlot = slot; node->lstate = LIMIT_INWINDOW; @@ -461,6 +515,16 @@ ExecLimit(PlanState *pstate) node->subSlot = slot; node->lstate = LIMIT_INWINDOW; } + if (node->limitOption == LIMIT_OPTION_PER_WITH_TIES) + { + if (tuplestore_gettupleslot_heaptuple(node->tupleStore, false, true, slot)) + { + node->subSlot = slot; + node->lstate = LIMIT_INWINDOW; + } + else + elog(ERROR, "LIMIT subplan failed to run backwards"); + } else { /* @@ -686,7 +750,8 @@ ExecInitLimit(Limit *node, EState *estate, int eflags) /* * Initialize the equality evaluation, to detect ties. */ - if (node->limitOption == LIMIT_OPTION_WITH_TIES) + if (node->limitOption == LIMIT_OPTION_WITH_TIES + || node->limitOption == LIMIT_OPTION_PER_WITH_TIES) { TupleDesc desc; const TupleTableSlotOps *ops; diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 99278eed93..c4a81dd2ad 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -2701,7 +2701,8 @@ create_limit_plan(PlannerInfo *root, LimitPath *best_path, int flags) subplan = create_plan_recurse(root, best_path->subpath, flags); /* Extract information necessary for comparing rows for WITH TIES. */ - if (best_path->limitOption == LIMIT_OPTION_WITH_TIES) + if (best_path->limitOption == LIMIT_OPTION_WITH_TIES || + best_path->limitOption == LIMIT_OPTION_PER_WITH_TIES) { Query *parse = root->parse; ListCell *l; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0168cb37f8..d18aad8722 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -11575,6 +11575,14 @@ limit_clause: n->limitOption = LIMIT_OPTION_WITH_TIES; $$ = n; } + | FETCH first_or_next select_fetch_first_value PERCENT row_or_rows WITH TIES + { + SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); + n->limitOffset = NULL; + n->limitCount = $3; + n->limitOption = LIMIT_OPTION_PER_WITH_TIES; + $$ = n; + } | FETCH first_or_next row_or_rows ONLY { SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); @@ -15921,7 +15929,8 @@ insertSelectOptions(SelectStmt *stmt, ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple limit options not allowed"))); - if (!stmt->sortClause && limitClause->limitOption == LIMIT_OPTION_WITH_TIES) + if (!stmt->sortClause && (limitClause->limitOption == LIMIT_OPTION_WITH_TIES + || limitClause->limitOption == LIMIT_OPTION_PER_WITH_TIES)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("WITH TIES options can not be specified without ORDER BY clause"))); diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index d3782697c8..2837771d1d 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -1754,7 +1754,9 @@ transformLimitClause(ParseState *pstate, Node *clause, return NULL; qual = transformExpr(pstate, clause, exprKind); - if (limitOption == LIMIT_OPTION_PERCENT && strcmp(constructName, "LIMIT") == 0) + if ((limitOption == LIMIT_OPTION_PERCENT || limitOption == LIMIT_OPTION_PER_WITH_TIES) + && strcmp(constructName, "LIMIT") == 0) + qual = coerce_to_specific_type(pstate, qual, FLOAT8OID, constructName); else qual = coerce_to_specific_type(pstate, qual, INT8OID, constructName); diff --git a/src/test/regress/expected/limit.out b/src/test/regress/expected/limit.out index 46557e2dae..7352632b6c 100644 --- a/src/test/regress/expected/limit.out +++ b/src/test/regress/expected/limit.out @@ -435,6 +435,43 @@ fetch all in c6; 4567890123456789 | 123 (3 rows) +declare c7 cursor for select * from int8_tbl order by q1 fetch first 15 percent rows with ties; +fetch all in c7; + q1 | q2 +-----+------------------ + 123 | 456 + 123 | 4567890123456789 +(2 rows) + +fetch 1 in c7; + q1 | q2 +----+---- +(0 rows) + +fetch backward 1 in c7; + q1 | q2 +-----+------------------ + 123 | 4567890123456789 +(1 row) + +fetch backward all in c7; + q1 | q2 +-----+----- + 123 | 456 +(1 row) + +fetch backward 1 in c7; + q1 | q2 +----+---- +(0 rows) + +fetch all in c7; + q1 | q2 +-----+------------------ + 123 | 456 + 123 | 4567890123456789 +(2 rows) + rollback; -- Stress test for variable LIMIT in conjunction with bounded-heap sorting SELECT @@ -716,6 +753,80 @@ SELECT thousand 0 (2 rows) +-- +-- FETCH FIRST +-- Check the PERCENT WITH TIES clause +-- +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 2 PERCENT ROW ONLY; + thousand +---------- + 0 +(1 row) + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 2 PERCENT ROWS WITH TIES; + thousand +---------- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 21 PERCENT ROW ONLY; + thousand +---------- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 +(11 rows) + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 21 PERCENT ROW WITH TIES; + thousand +---------- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +(20 rows) + -- should fail SELECT ''::text AS two, unique1, unique2, stringu1 FROM onek WHERE unique1 > 50 diff --git a/src/test/regress/sql/limit.sql b/src/test/regress/sql/limit.sql index c6e660913f..f9e3d24910 100644 --- a/src/test/regress/sql/limit.sql +++ b/src/test/regress/sql/limit.sql @@ -105,6 +105,14 @@ fetch backward all in c6; fetch backward 1 in c6; fetch all in c6; +declare c7 cursor for select * from int8_tbl order by q1 fetch first 15 percent rows with ties; +fetch all in c7; +fetch 1 in c7; +fetch backward 1 in c7; +fetch backward all in c7; +fetch backward 1 in c7; +fetch all in c7; + rollback; -- Stress test for variable LIMIT in conjunction with bounded-heap sorting @@ -197,6 +205,27 @@ SELECT thousand FROM onek WHERE thousand < 5 ORDER BY thousand FETCH FIRST 2 ROW ONLY; +-- +-- FETCH FIRST +-- Check the PERCENT WITH TIES clause +-- + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 2 PERCENT ROW ONLY; + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 2 PERCENT ROWS WITH TIES; + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 21 PERCENT ROW ONLY; + +SELECT thousand + FROM onek WHERE thousand < 5 + ORDER BY thousand FETCH FIRST 21 PERCENT ROW WITH TIES; + -- should fail SELECT ''::text AS two, unique1, unique2, stringu1 FROM onek WHERE unique1 > 50