diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 4dd9d029e6..d61769da75 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -14721,8 +14721,8 @@ SELECT xmlagg(x) FROM (SELECT x FROM test ORDER BY y DESC) AS tab;
    partition through the last peer of the current row.  This is
    likely to give unhelpful results for <function>last_value</function> and
    sometimes also <function>nth_value</function>.  You can redefine the frame by
-   adding a suitable frame specification (<literal>RANGE</literal> or
-   <literal>ROWS</literal>) to the <literal>OVER</literal> clause.
+   adding a suitable frame specification (<literal>RANGE</literal>,
+   <literal>ROWS</literal> or <literal>GROUPS</literal>) to the <literal>OVER</literal> clause.
    See <xref linkend="syntax-window-functions"/> for more information
    about frame specifications.
   </para>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 40c146ca40..26ff2f59fa 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -859,8 +859,8 @@ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceabl
     The <replaceable class="parameter">frame_clause</replaceable> can be one of
 
 <synopsis>
-{ RANGE | ROWS } <replaceable>frame_start</replaceable> [ <replaceable class="parameter">frame_exclusion_clause</replaceable> ]
-{ RANGE | ROWS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable class="parameter">frame_exclusion_clause</replaceable> ]
+{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable class="parameter">frame_exclusion_clause</replaceable> ]
+{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable class="parameter">frame_exclusion_clause</replaceable> ]
 </synopsis>
 
     where <replaceable>frame_start</replaceable> and <replaceable>frame_end</replaceable> can be
@@ -879,6 +879,7 @@ UNBOUNDED FOLLOWING
 
 <synopsis>
 EXCLUDE CURRENT ROW
+EXCLUDE GROUP
 EXCLUDE TIES
 EXCLUDE NO OTHERS
 </synopsis>
@@ -903,19 +904,20 @@ EXCLUDE NO OTHERS
     In general, <literal>UNBOUNDED PRECEDING</literal> means that the frame
     starts with the first row of the partition, and similarly
     <literal>UNBOUNDED FOLLOWING</literal> means that the frame ends with the last
-    row of the partition (regardless of <literal>RANGE</literal> or <literal>ROWS</literal>
-    mode).  In <literal>ROWS</literal> mode, <literal>CURRENT ROW</literal>
+    row of the partition (regardless of <literal>RANGE</literal>, <literal>ROWS</literal>
+    or <literal>GROUPS</literal> mode).  In <literal>ROWS</literal> mode, <literal>CURRENT ROW</literal>
     means that the frame starts or ends with the current row; but in
-    <literal>RANGE</literal> mode it means that the frame starts or ends with
+    <literal>RANGE</literal> or <literal>GROUPS</literal> mode it means that the frame starts or ends with
     the current row's first or last peer in the <literal>ORDER BY</literal> ordering.
     The <replaceable>value</replaceable> <literal>PRECEDING</literal> and
     <replaceable>value</replaceable> <literal>FOLLOWING</literal> cases differ depending on
-    whether the frame clause is in <literal>ROWS</literal> or <literal>RANGE</literal> mode. In
+    whether the frame clause is in <literal>ROWS</literal>, <literal>RANGE</literal>or <literal>GROUPS</literal> mode. In
     <literal>ROWS</literal> mode, they indicate that the frame starts or ends with the row that
     many rows before or after the current row. In <literal>RANGE</literal> mode, they indicate that
     the frame starts or ends when the ORDER BY column's value for each row is within the bounds
-    specified by <replaceable>value</replaceable> for both the start and the end of the frame.
-    In <literal>ROWS</literal> mode, <replaceable>value</replaceable> must be an integer expression not
+    specified by <replaceable>value</replaceable> for both the start and the end of the frame. In <literal>GROUPS</literal> mode,
+    they indicate the number of changes to the value of the ORDER BY columns (i.e., groups of peers).
+    In <literal>ROWS</literal> or <literal>GROUPS</literal> mode, <replaceable>value</replaceable> must be an integer expression not
     containing any variables, aggregate functions, or window functions.In <literal>RANGE</literal> mode,
     there must be exactly one ORDER BY column and if the column is an integer column,
     then <replaceable>value</replaceable> must be an integer. If it is a date/time column, then
@@ -926,14 +928,15 @@ EXCLUDE NO OTHERS
    <para>
     For the <replaceable class="parameter">frame_exclusion_clause</replaceable>, <literal>EXCLUDE CURRENT ROW</literal>
     excludes the current row from the frame. <literal>EXCLUDE TIES</literal> excludes any peers of the current row from the
-    frame. <literal>EXCLUDE NO OTHERS</literal> does nothing, but is provided in order to optionally document the intention
+    frame. <literal>EXCLUDE GROUP</literal> excludes both the current row and any peers of the current row from the frame.
+    <literal>EXCLUDE NO OTHERS</literal> does nothing, but is provided in order to optionally document the intention
     not to exclude any other rows.
    </para>
 
    <para>
     Beware that the <literal>ROWS</literal> options can produce unpredictable
     results if the <literal>ORDER BY</literal> ordering does not order the rows
-    uniquely.  The <literal>RANGE</literal> options are designed to ensure that
+    uniquely.  The <literal>RANGE</literal> and <literal>GROUPS</literal> options are designed to ensure that
     rows that are peers in the <literal>ORDER BY</literal> ordering are treated
     alike; all peer rows will be in the same frame.
    </para>
diff --git a/doc/src/sgml/syntax.sgml b/doc/src/sgml/syntax.sgml
index f711c99987..87905ef2e2 100644
--- a/doc/src/sgml/syntax.sgml
+++ b/doc/src/sgml/syntax.sgml
@@ -1805,8 +1805,8 @@ FROM generate_series(1,10) AS s(i);
     and the optional <replaceable class="parameter">frame_clause</replaceable>
     can be one of
 <synopsis>
-{ RANGE | ROWS } <replaceable>frame_start</replaceable> [ <replaceable class="parameter">frame_exclusion_clause</replaceable> ]
-{ RANGE | ROWS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable class="parameter">frame_exclusion_clause</replaceable> ]
+{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable class="parameter">frame_exclusion_clause</replaceable> ]
+{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable class="parameter">frame_exclusion_clause</replaceable> ]
 </synopsis>
     where <replaceable>frame_start</replaceable> and <replaceable>frame_end</replaceable> can be
     one of
@@ -1820,6 +1820,7 @@ UNBOUNDED FOLLOWING
     where the optional <replaceable>frame_exclusion_clause</replaceable> can be one of
 <synopsis>
 EXCLUDE CURRENT ROW
+EXCLUDE GROUP
 EXCLUDE TIES
 EXCLUDE NO OTHERS
 </synopsis>
@@ -1863,8 +1864,8 @@ EXCLUDE NO OTHERS
     the set of rows constituting the <firstterm>window frame</firstterm>, which is a
     subset of the current partition, for those window functions that act on
     the frame instead of the whole partition.  The frame can be specified in
-    either <literal>RANGE</literal> or <literal>ROWS</literal> mode; in either case, it
-    runs from the <replaceable>frame_start</replaceable> to the
+    either <literal>RANGE</literal>, <literal>ROWS</literal> or <literal>GROUPS</literal> mode;
+    in each case, it runs from the <replaceable>frame_start</replaceable> to the
     <replaceable>frame_end</replaceable>.  If <replaceable>frame_end</replaceable> is omitted,
     it defaults to <literal>CURRENT ROW</literal>.
    </para>
@@ -1877,7 +1878,7 @@ EXCLUDE NO OTHERS
    </para>
 
    <para>
-    In <literal>RANGE</literal> mode, a <replaceable>frame_start</replaceable> of
+    In <literal>RANGE</literal> or <literal>GROUPS</literal> mode, a <replaceable>frame_start</replaceable> of
     <literal>CURRENT ROW</literal> means the frame starts with the current row's
     first <firstterm>peer</firstterm> row (a row that <literal>ORDER BY</literal> considers
     equivalent to the current row), while a <replaceable>frame_end</replaceable> of
@@ -1903,10 +1904,19 @@ EXCLUDE NO OTHERS
    </para>
 
    <para>
+    In <literal>GROUPS</literal> mode, <replaceable>value</replaceable> <literal>PRECEDING</literal> and
+    <replaceable>value</replaceable> <literal>FOLLOWING</literal> cases indicate that the frame starts or ends the specified
+    number of <literal>window framing groups</literal> before or after the current <literal>window framing group</literal>.
+    Two rows are in the same <literal>window framing group</literal> if they are peers, (i.e., their ORDER BY column values
+    match). This mode allows the selection of a frame by the number of changes to the ORDER BY columns.
+   </para>
+
+   <para>
     For the <replaceable class="parameter">frame_exclusion_clause</replaceable>, <literal>EXCLUDE CURRENT ROW</literal>
     excludes the current row from the frame. <literal>EXCLUDE TIES</literal> excludes any peers of the current row from the
-    frame. <literal>EXCLUDE NO OTHERS</literal> does nothing, but is provided in order to optionally document the intention
-    not to exclude any other rows.
+    frame. <literal>EXCLUDE GROUP</literal> excludes both the current row and any peers of the current row from the frame.
+    <literal>EXCLUDE NO OTHERS</literal> does nothing, but is provided in order to optionally document the intention not to
+    exclude any other rows.
    </para>
 
    <para>
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 8e746f36d4..20d61f3780 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -498,7 +498,7 @@ T616	Null treatment option for LEAD and LAG functions			NO
 T617	FIRST_VALUE and LAST_VALUE function			YES	
 T618	NTH_VALUE function			NO	function exists, but some options missing
 T619	Nested window functions			NO	
-T620	WINDOW clause: GROUPS option			NO	
+T620	WINDOW clause: GROUPS option			YES	
 T621	Enhanced numeric functions			YES	
 T631	IN predicate with one list element			YES	
 T641	Multiple column assignment			NO	only some syntax variants supported
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4c6199fbe4..4e5b98f847 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -68,6 +68,8 @@ typedef struct WindowObjectData
 	int			readptr;		/* tuplestore read pointer for this fn */
 	int64		markpos;		/* row that markptr is positioned on */
 	int64		seekpos;		/* row that readptr is positioned on */
+	int64		winGroupsCount;	/* number of window groups, used for GROUPS with values */
+	int64		*winGroupLen;	/* length of each window group */
 } WindowObjectData;
 
 /*
@@ -181,6 +183,8 @@ static void begin_partition(WindowAggState *winstate);
 static void spool_tuples(WindowAggState *winstate, int64 pos);
 static void release_partition(WindowAggState *winstate);
 
+static bool row_is_in_group(WindowObject winobj, int64 currpos, int64 slotpos,
+				int64 offset, bool preceding, bool end);
 static bool row_is_in_range(Oid sortColOid, Datum currval,
 				Datum slotval, Datum offset, bool preceding, bool end);
 static int row_is_in_frame(WindowAggState *winstate, int64 pos,
@@ -686,9 +690,6 @@ eval_windowaggregates(WindowAggState *winstate)
 	temp_slot = winstate->temp_slot_1;
 
 	/*
-	 * Currently, we support only a subset of the SQL-standard window framing
-	 * rules.
-	 *
 	 * If the frame start is UNBOUNDED_PRECEDING, the window frame consists of
 	 * a contiguous group of rows extending forward from the start of the
 	 * partition, and rows only enter the frame, never exit it, as the current
@@ -740,16 +741,17 @@ eval_windowaggregates(WindowAggState *winstate)
 	 * the result values that were previously saved at the bottom of this
 	 * function.  Since we don't know the current frame's end yet, this is not
 	 * possible to check for fully.  But if the frame end mode is UNBOUNDED
-	 * FOLLOWING or CURRENT ROW, no exclusion clause is specified, and the
-	 * current row lies within the previous row's frame, then the two frames'
-	 * ends must coincide.  Note that on the first row
-	 * aggregatedbase == aggregatedupto, meaning this test must fail, so we
+	 * FOLLOWING or CURRENT ROW, no exclusion clause is specified, we are not
+	 * in GROUPS BETWEEN with values mode, and the current row lies within the
+	 * previous row's frame, then the two frames' ends must coincide.  Note that on
+	 * the first row aggregatedbase == aggregatedupto, meaning this test must fail, so we
 	 * don't need to check the "there was no previous row" case explicitly here.
 	 */
 	if (winstate->aggregatedbase == winstate->frameheadpos &&
 		(winstate->frameOptions & (FRAMEOPTION_END_UNBOUNDED_FOLLOWING |
 								   FRAMEOPTION_END_CURRENT_ROW)) &&
 		!(winstate->frameOptions & FRAMEOPTION_EXCLUSION) &&
+		!(winstate->frameOptions & FRAMEOPTION_GROUPS_BETWEEN) &&
 		winstate->aggregatedbase <= winstate->currentpos &&
 		winstate->aggregatedupto > winstate->currentpos)
 	{
@@ -771,6 +773,7 @@ eval_windowaggregates(WindowAggState *winstate)
 	 *	 - if the frame's head moved and we cannot use an inverse
 	 *	   transition function, or
 	 *	 - we are in RANGE BETWEEN with values mode, or
+	 *	 - we are in GROUPS BETWEEN with values mode, or
 	 *	 - we are in EXCLUDE CURRENT ROW/EXCLUDE TIES mode, or
 	 *	 - if the new frame doesn't overlap the old one
 	 *
@@ -787,6 +790,7 @@ eval_windowaggregates(WindowAggState *winstate)
 			(winstate->aggregatedbase != winstate->frameheadpos &&
 			 !OidIsValid(peraggstate->invtransfn_oid)) ||
 			winstate->frameOptions & FRAMEOPTION_RANGE_BETWEEN ||
+			winstate->frameOptions & FRAMEOPTION_GROUPS_BETWEEN ||
 			winstate->frameOptions & FRAMEOPTION_EXCLUSION ||
 			winstate->aggregatedupto <= winstate->frameheadpos)
 		{
@@ -1132,6 +1136,7 @@ begin_partition(WindowAggState *winstate)
 		/* If the frame head is potentially movable ... */
 		if (!(winstate->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) ||
 			winstate->frameOptions & (FRAMEOPTION_RANGE_BETWEEN) ||
+			winstate->frameOptions & (FRAMEOPTION_GROUPS_BETWEEN) ||
 			winstate->frameOptions & (FRAMEOPTION_EXCLUSION))
 		{
 			/* ... create a mark pointer to track the frame head */
@@ -1174,6 +1179,51 @@ begin_partition(WindowAggState *winstate)
 	 */
 	tuplestore_puttupleslot(winstate->buffer, winstate->first_part_slot);
 	winstate->spooled_rows++;
+
+	/*
+	 * In GROUPS BETWEEN with values mode, we pre-compute the lengths of each
+	 * window group by loading the partition and checking if each row is a peer
+	 * of the succeeding row. The number of groups is stored in winobj->winGroupsCount
+	 * and the number of rows in each group is stored in winobj->winGroupLen.
+	 */
+	if (winstate->frameOptions & FRAMEOPTION_GROUPS_BETWEEN)
+	{
+		int64	i,
+				partitionSize;
+		int64	*lenptr;
+		int64	groupLenSize = 16;
+		bool	peers;
+
+		winstate->agg_winobj->winGroupsCount = 1;
+		winstate->agg_winobj->winGroupLen = palloc0(sizeof(int64) * groupLenSize);
+		lenptr = winstate->agg_winobj->winGroupLen;
+		*lenptr = 1;
+		partitionSize = WinGetPartitionRowCount(winstate->agg_winobj);
+
+		for (i = 0; i < partitionSize - 1; i++)
+		{
+			peers = WinRowsArePeers(winstate->agg_winobj, i, i+1);
+			if (peers)
+				(*lenptr)++;
+			else
+			{
+				winstate->agg_winobj->winGroupsCount++;
+				if (winstate->agg_winobj->winGroupsCount > groupLenSize)
+				{
+					int64 prevSize = groupLenSize;
+
+					groupLenSize *= 2;
+					winstate->agg_winobj->winGroupLen = repalloc(winstate->agg_winobj->winGroupLen, sizeof(int64) * groupLenSize);
+					lenptr = winstate->agg_winobj->winGroupLen;
+					lenptr += prevSize;
+				}
+				else
+					lenptr++;
+
+				*lenptr = 1;
+			}
+		}
+	}
 }
 
 /*
@@ -1284,6 +1334,71 @@ release_partition(WindowAggState *winstate)
 		tuplestore_end(winstate->buffer);
 	winstate->buffer = NULL;
 	winstate->partition_spooled = false;
+
+	if (winstate->frameOptions & FRAMEOPTION_GROUPS_BETWEEN &&
+		winstate->agg_winobj->winGroupsCount > 0)
+	{
+		pfree(winstate->agg_winobj->winGroupLen);
+		winstate->agg_winobj->winGroupsCount = 0;
+	}
+}
+
+/*
+ * row_is_in_group
+ * Determine whether a row is in range when in GROUPS BETWEEN with values
+ * mode.
+ *
+ * Compares the current position to the slot position and checks if they are
+ * within the specified window group offset.
+ */
+static bool row_is_in_group(WindowObject winobj, int64 currpos, int64 slotpos,
+				int64 offset, bool preceding, bool end)
+{
+	int64		i,
+				len = 0,
+				currGroup = 0,
+				slotGroup = 0;
+	int64		*lenptr = winobj->winGroupLen;
+
+	if (preceding)
+		offset = -offset;
+	/*
+	* Calculate the currpos window group, then the slotpos window group. If
+	* the slotpos group is outside of the offset bounds, return false.
+	*/
+	for (i = 0; i < winobj->winGroupsCount; i++, lenptr++)
+	{
+		len += *lenptr;
+		if (len > currpos)
+		{
+			currGroup = i;
+			break;
+		}
+	}
+	lenptr = winobj->winGroupLen;
+	len = 0;
+	for (i = 0; i < winobj->winGroupsCount; i++, lenptr++)
+	{
+		len += *lenptr;
+		if (len > slotpos)
+		{
+			slotGroup = i;
+			break;
+		}
+	}
+
+	currGroup += offset;
+	if (end)
+	{
+		if (slotGroup > currGroup)
+			return false;
+	}
+	else
+	{
+		if (slotGroup < currGroup)
+			return false;
+	}
+	return true;
 }
 
 /*
@@ -1445,7 +1560,7 @@ row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot)
 			if (pos < winstate->currentpos)
 				return -1;
 		}
-		else if (frameOptions & FRAMEOPTION_RANGE)
+		else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS))
 		{
 			/* preceding row that is not peer is out of frame */
 			if (pos < winstate->currentpos &&
@@ -1483,6 +1598,15 @@ row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot)
 				slotval, winstate->startOffsetValue, preceding, false))
 				return 0;
 		}
+		else if (frameOptions & FRAMEOPTION_GROUPS)
+		{
+			bool		preceding;
+
+			preceding = (frameOptions & FRAMEOPTION_START_VALUE_PRECEDING) != 0 ? true : false;
+			if (!row_is_in_group(winstate->agg_winobj,
+								 winstate->currentpos, pos, offset, preceding, false))
+				return 0;
+		}
 		else
 			Assert(false);
 	}
@@ -1496,7 +1620,7 @@ row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot)
 			if (pos > winstate->currentpos)
 				return -1;
 		}
-		else if (frameOptions & FRAMEOPTION_RANGE)
+		else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS))
 		{
 			/* following row that is not peer is out of frame */
 			if (pos > winstate->currentpos &&
@@ -1533,6 +1657,15 @@ row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot)
 				slotval, winstate->endOffsetValue, preceding, true))
 				return -1;
 		}
+		else if (frameOptions & FRAMEOPTION_GROUPS)
+		{
+			bool		preceding;
+
+			preceding = (frameOptions & FRAMEOPTION_END_VALUE_PRECEDING) != 0 ? true : false;
+			if (!row_is_in_group(winstate->agg_winobj,
+								 winstate->currentpos, pos, offset, preceding, true))
+				return -1;
+		}
 		else
 			Assert(false);
 	}
@@ -1542,7 +1675,12 @@ row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot)
 	{
 		if (pos == winstate->currentpos)
 			return 0;
-	} else if (frameOptions & FRAMEOPTION_EXCLUDE_TIES)
+	} else if (frameOptions & FRAMEOPTION_EXCLUDE_GROUP)
+	{
+		if (are_peers(winstate, slot, winstate->ss.ss_ScanTupleSlot))
+			return 0;
+	}
+	else if (frameOptions & FRAMEOPTION_EXCLUDE_TIES)
 	{
 		if ((pos != winstate->currentpos) &&
 			are_peers(winstate, slot, winstate->ss.ss_ScanTupleSlot))
@@ -1586,7 +1724,7 @@ update_frameheadpos(WindowObject winobj, TupleTableSlot *slot)
 			winstate->frameheadpos = winstate->currentpos;
 			winstate->framehead_valid = true;
 		}
-		else if (frameOptions & FRAMEOPTION_RANGE)
+		else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS))
 		{
 			int64		fhprev;
 
@@ -1646,7 +1784,7 @@ update_frameheadpos(WindowObject winobj, TupleTableSlot *slot)
 			}
 			winstate->framehead_valid = true;
 		}
-		else if (frameOptions & FRAMEOPTION_RANGE)
+		else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS))
 		{
 			winstate->frameheadpos = 0;
 			winstate->framehead_valid = true;
@@ -1692,7 +1830,7 @@ update_frametailpos(WindowObject winobj, TupleTableSlot *slot)
 			winstate->frametailpos = winstate->currentpos;
 			winstate->frametail_valid = true;
 		}
-		else if (frameOptions & FRAMEOPTION_RANGE)
+		else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS))
 		{
 			int64		ftnext;
 
@@ -1748,7 +1886,7 @@ update_frametailpos(WindowObject winobj, TupleTableSlot *slot)
 			}
 			winstate->frametail_valid = true;
 		}
-		else if (frameOptions & FRAMEOPTION_RANGE)
+		else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS))
 		{
 			winstate->frametailpos = -1;
 			winstate->frametail_valid = true;
@@ -1809,7 +1947,7 @@ ExecWindowAgg(PlanState *pstate)
 			get_typlenbyval(exprType((Node *) winstate->startOffset->expr),
 							&len, &byval);
 			winstate->startOffsetValue = datumCopy(value, byval, len);
-			if (frameOptions & FRAMEOPTION_ROWS)
+			if (frameOptions & (FRAMEOPTION_ROWS | FRAMEOPTION_GROUPS))
 			{
 				/* value is known to be int8 */
 				int64		offset = DatumGetInt64(value);
@@ -1834,7 +1972,7 @@ ExecWindowAgg(PlanState *pstate)
 			get_typlenbyval(exprType((Node *) winstate->endOffset->expr),
 							&len, &byval);
 			winstate->endOffsetValue = datumCopy(value, byval, len);
-			if (frameOptions & FRAMEOPTION_ROWS)
+			if (frameOptions & (FRAMEOPTION_ROWS | FRAMEOPTION_GROUPS))
 			{
 				/* value is known to be int8 */
 				int64		offset = DatumGetInt64(value);
@@ -1852,6 +1990,7 @@ ExecWindowAgg(PlanState *pstate)
 	{
 		/* Initialize for first partition and set current row = 0 */
 		begin_partition(winstate);
+
 		/* If there are no input rows, we'll detect that and exit below */
 	}
 	else
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d54fbff545..917341f1c5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -632,7 +632,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	FALSE_P FAMILY FETCH FILTER FIRST_P FLOAT_P FOLLOWING FOR
 	FORCE FOREIGN FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
 
-	GENERATED GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING
+	GENERATED GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING GROUPS
 
 	HANDLER HAVING HEADER_P HOLD HOUR_P
 
@@ -725,8 +725,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  * various unreserved keywords as needed to resolve ambiguities (this can't
  * have any bad effects since obviously the keywords will still behave the
  * same as if they weren't keywords).  We need to do this for PARTITION,
- * RANGE, ROWS to support opt_existing_window_name; and for RANGE, ROWS
- * so that they can follow a_expr without creating postfix-operator problems;
+ * RANGE, ROWS, or GROUPS to support opt_existing_window_name; and for RANGE, ROWS
+ * or GROUPS so that they can follow a_expr without creating postfix-operator problems;
  * for GENERATED so that it can follow b_expr;
  * and for NULL so that it can follow b_expr in ColQualList without creating
  * postfix-operator problems.
@@ -746,7 +746,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  * blame any funny behavior of UNBOUNDED on the SQL standard, though.
  */
 %nonassoc	UNBOUNDED		/* ideally should have same precedence as IDENT */
-%nonassoc	IDENT GENERATED NULL_P PARTITION RANGE ROWS PRECEDING FOLLOWING CUBE ROLLUP
+%nonassoc	IDENT GENERATED NULL_P PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
 %left		Op OPERATOR		/* multi-character ops and user-defined operators */
 %left		'+' '-'
 %left		'*' '/' '%'
@@ -13981,7 +13981,7 @@ window_specification: '(' opt_existing_window_name opt_partition_clause
 		;
 
 /*
- * If we see PARTITION, RANGE, or ROWS as the first token after the '('
+ * If we see PARTITION, RANGE, ROWS or GROUPS as the first token after the '('
  * of a window_specification, we want the assumption to be that there is
  * no existing_window_name; but those keywords are unreserved and so could
  * be ColIds.  We fix this by making them have the same precedence as IDENT
@@ -14001,9 +14001,6 @@ opt_partition_clause: PARTITION BY expr_list		{ $$ = $3; }
 /*
  * For frame clauses, we return a WindowDef, but only some fields are used:
  * frameOptions, startOffset, and endOffset.
- *
- * This is only a subset of the full SQL:2011 frame_clause grammar.
- * We don't support GROUPS yet.
  */
 opt_frame_clause:
 			RANGE frame_extent opt_window_exclusion_clause
@@ -14029,6 +14026,20 @@ opt_frame_clause:
 						n->frameOptions |= n2->frameOptions;
 					$$ = n;
 				}
+			| GROUPS frame_extent opt_window_exclusion_clause
+				{
+					WindowDef *n = $2;
+					WindowDef *n2 = $3;
+					n->frameOptions |= FRAMEOPTION_NONDEFAULT | FRAMEOPTION_GROUPS;
+					if ((n->frameOptions & (FRAMEOPTION_START_VALUE_PRECEDING |
+										   FRAMEOPTION_END_VALUE_PRECEDING)) ||
+						(n->frameOptions & (FRAMEOPTION_START_VALUE_FOLLOWING |
+										   FRAMEOPTION_END_VALUE_FOLLOWING)))
+							n->frameOptions |= FRAMEOPTION_GROUPS_BETWEEN;
+					if (n2 != NULL)
+						n->frameOptions |= n2->frameOptions;
+					$$ = n;
+				}
 			| /*EMPTY*/
 				{
 					WindowDef *n = makeNode(WindowDef);
@@ -14150,6 +14161,12 @@ opt_window_exclusion_clause:
 					n->frameOptions = FRAMEOPTION_EXCLUDE_CURRENT;
 					$$ = n;
 				}
+			| EXCLUDE GROUP_P
+				{
+					WindowDef *n = makeNode(WindowDef);
+					n->frameOptions = FRAMEOPTION_EXCLUDE_GROUP;
+					$$ = n;
+				}
 			| EXCLUDE TIES
 				{
 					WindowDef *n = makeNode(WindowDef);
@@ -15026,6 +15043,7 @@ unreserved_keyword:
 			| GENERATED
 			| GLOBAL
 			| GRANTED
+			| GROUPS
 			| HANDLER
 			| HEADER_P
 			| HOLD
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 4c4f4cdc3d..0922a9a7c3 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -420,6 +420,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 				err = _("grouping operations are not allowed in window ROWS");
 
 			break;
+		case EXPR_KIND_WINDOW_FRAME_GROUPS:
+			if (isAgg)
+				err = _("aggregate functions are not allowed in window GROUPS");
+			else
+				err = _("grouping operations are not allowed in window GROUPS");
+
+			break;
 		case EXPR_KIND_SELECT_TARGET:
 			/* okay */
 			break;
@@ -835,6 +842,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_WINDOW_ORDER:
 		case EXPR_KIND_WINDOW_FRAME_RANGE:
 		case EXPR_KIND_WINDOW_FRAME_ROWS:
+		case EXPR_KIND_WINDOW_FRAME_GROUPS:
 			err = _("window functions are not allowed in window definitions");
 			break;
 		case EXPR_KIND_SELECT_TARGET:
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 73c80bbf1c..7d92a8edfd 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -3588,6 +3588,17 @@ transformFrameOffset(ParseState *pstate, int frameOptions, Node *clause)
 		else
 			node = coerce_to_specific_type(pstate, node, INT8OID, constructName);
 	}
+	else if (frameOptions & FRAMEOPTION_GROUPS)
+	{
+		/* Transform the raw expression tree */
+		node = transformExpr(pstate, clause, EXPR_KIND_WINDOW_FRAME_GROUPS);
+
+		/*
+		 * Like LIMIT clause, simply coerce to int8
+		 */
+		constructName = "GROUPS";
+		node = coerce_to_specific_type(pstate, node, INT8OID, constructName);
+	}
 	else
 	{
 		Assert(false);
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 29f9da796f..a1f44b0e7a 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1805,6 +1805,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_WINDOW_ORDER:
 		case EXPR_KIND_WINDOW_FRAME_RANGE:
 		case EXPR_KIND_WINDOW_FRAME_ROWS:
+		case EXPR_KIND_WINDOW_FRAME_GROUPS:
 		case EXPR_KIND_SELECT_TARGET:
 		case EXPR_KIND_INSERT_TARGET:
 		case EXPR_KIND_UPDATE_SOURCE:
@@ -3428,6 +3429,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "window RANGE";
 		case EXPR_KIND_WINDOW_FRAME_ROWS:
 			return "window ROWS";
+		case EXPR_KIND_WINDOW_FRAME_GROUPS:
+			return "window GROUPS";
 		case EXPR_KIND_SELECT_TARGET:
 			return "SELECT";
 		case EXPR_KIND_INSERT_TARGET:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 2f20516e76..9b8413bf26 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2218,6 +2218,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 			break;
 		case EXPR_KIND_WINDOW_FRAME_RANGE:
 		case EXPR_KIND_WINDOW_FRAME_ROWS:
+		case EXPR_KIND_WINDOW_FRAME_GROUPS:
 			err = _("set-returning functions are not allowed in window definitions");
 			break;
 		case EXPR_KIND_SELECT_TARGET:
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 867a4a26a8..85dc7d654c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -5874,6 +5874,8 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
 			appendStringInfoString(buf, "RANGE ");
 		else if (wc->frameOptions & FRAMEOPTION_ROWS)
 			appendStringInfoString(buf, "ROWS ");
+		else if (wc->frameOptions & FRAMEOPTION_GROUPS)
+			appendStringInfoString(buf, "GROUPS ");
 		else
 			Assert(false);
 		if (wc->frameOptions & FRAMEOPTION_BETWEEN)
@@ -5916,6 +5918,8 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
 		}
 		if (wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT)
 			appendStringInfoString(buf, "EXCLUDE CURRENT ROW ");
+		else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP)
+			appendStringInfoString(buf, "EXCLUDE GROUP ");
 		else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES)
 			appendStringInfoString(buf, "EXCLUDE TIES ");
 		else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_NO_OTHERS)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5cfce277cc..626c1864a9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -499,26 +499,28 @@ typedef struct WindowDef
  * which were defaulted; the correct behavioral bits must be set either way.
  * The START_foo and END_foo options must come in pairs of adjacent bits for
  * the convenience of gram.y, even though some of them are useless/invalid.
- * We will need more bits (and fields) to cover the full SQL:2008 option set.
  */
 #define FRAMEOPTION_NONDEFAULT					0x00001 /* any specified? */
 #define FRAMEOPTION_RANGE						0x00002 /* RANGE behavior */
 #define FRAMEOPTION_ROWS						0x00004 /* ROWS behavior */
-#define FRAMEOPTION_BETWEEN						0x00008 /* BETWEEN given? */
-#define FRAMEOPTION_START_UNBOUNDED_PRECEDING	0x00010 /* start is U. P. */
-#define FRAMEOPTION_END_UNBOUNDED_PRECEDING		0x00020 /* (disallowed) */
-#define FRAMEOPTION_START_UNBOUNDED_FOLLOWING	0x00040 /* (disallowed) */
-#define FRAMEOPTION_END_UNBOUNDED_FOLLOWING		0x00080 /* end is U. F. */
-#define FRAMEOPTION_START_CURRENT_ROW			0x00100 /* start is C. R. */
-#define FRAMEOPTION_END_CURRENT_ROW				0x00200 /* end is C. R. */
-#define FRAMEOPTION_START_VALUE_PRECEDING		0x00400 /* start is V. P. */
-#define FRAMEOPTION_END_VALUE_PRECEDING			0x00800 /* end is V. P. */
-#define FRAMEOPTION_START_VALUE_FOLLOWING		0x01000 /* start is V. F. */
-#define FRAMEOPTION_END_VALUE_FOLLOWING			0x02000 /* end is V. F. */
-#define FRAMEOPTION_RANGE_BETWEEN				0x04000 /* RANGE BETWEEN with values */
-#define FRAMEOPTION_EXCLUDE_CURRENT				0x08000 /* exclude current row */
-#define FRAMEOPTION_EXCLUDE_TIES				0x10000 /* exclude ties */
-#define FRAMEOPTION_EXCLUDE_NO_OTHERS			0x20000 /* exclude no others */
+#define FRAMEOPTION_GROUPS						0x00008 /* GROUPS behavior */
+#define FRAMEOPTION_BETWEEN						0x00010 /* BETWEEN given? */
+#define FRAMEOPTION_START_UNBOUNDED_PRECEDING	0x00020 /* start is U. P. */
+#define FRAMEOPTION_END_UNBOUNDED_PRECEDING		0x00040 /* (disallowed) */
+#define FRAMEOPTION_START_UNBOUNDED_FOLLOWING	0x00080 /* (disallowed) */
+#define FRAMEOPTION_END_UNBOUNDED_FOLLOWING		0x00100 /* end is U. F. */
+#define FRAMEOPTION_START_CURRENT_ROW			0x00200 /* start is C. R. */
+#define FRAMEOPTION_END_CURRENT_ROW				0x00400 /* end is C. R. */
+#define FRAMEOPTION_START_VALUE_PRECEDING		0x00800 /* start is V. P. */
+#define FRAMEOPTION_END_VALUE_PRECEDING			0x01000 /* end is V. P. */
+#define FRAMEOPTION_START_VALUE_FOLLOWING		0x02000 /* start is V. F. */
+#define FRAMEOPTION_END_VALUE_FOLLOWING			0x04000 /* end is V. F. */
+#define FRAMEOPTION_RANGE_BETWEEN				0x08000 /* RANGE BETWEEN with values */
+#define FRAMEOPTION_GROUPS_BETWEEN				0x010000 /* GROUPS BETWEEN with values */
+#define FRAMEOPTION_EXCLUDE_CURRENT				0x020000 /* exclude current row */
+#define FRAMEOPTION_EXCLUDE_TIES				0x040000 /* exclude ties */
+#define FRAMEOPTION_EXCLUDE_NO_OTHERS			0x080000 /* exclude no others */
+#define FRAMEOPTION_EXCLUDE_GROUP				0x100000 /* exclude group */
 
 #define FRAMEOPTION_START_VALUE \
 	(FRAMEOPTION_START_VALUE_PRECEDING | FRAMEOPTION_START_VALUE_FOLLOWING)
@@ -530,7 +532,8 @@ typedef struct WindowDef
 	 FRAMEOPTION_END_CURRENT_ROW)
 
 #define FRAMEOPTION_EXCLUSION \
-	(FRAMEOPTION_EXCLUDE_CURRENT | FRAMEOPTION_EXCLUDE_TIES)
+	(FRAMEOPTION_EXCLUDE_CURRENT | FRAMEOPTION_EXCLUDE_TIES | \
+	 FRAMEOPTION_EXCLUDE_GROUP)
 
 /*
  * RangeSubselect - subquery appearing in a FROM clause
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 87c4cffba5..b51f86fd28 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -182,6 +182,7 @@ PG_KEYWORD("granted", GRANTED, UNRESERVED_KEYWORD)
 PG_KEYWORD("greatest", GREATEST, COL_NAME_KEYWORD)
 PG_KEYWORD("group", GROUP_P, RESERVED_KEYWORD)
 PG_KEYWORD("grouping", GROUPING, COL_NAME_KEYWORD)
+PG_KEYWORD("groups", GROUPS, UNRESERVED_KEYWORD)
 PG_KEYWORD("handler", HANDLER, UNRESERVED_KEYWORD)
 PG_KEYWORD("having", HAVING, RESERVED_KEYWORD)
 PG_KEYWORD("header", HEADER_P, UNRESERVED_KEYWORD)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 565bb3dc6c..1141d8aa2f 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -45,6 +45,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_WINDOW_ORDER,		/* window definition ORDER BY */
 	EXPR_KIND_WINDOW_FRAME_RANGE,	/* window frame clause with RANGE */
 	EXPR_KIND_WINDOW_FRAME_ROWS,	/* window frame clause with ROWS */
+	EXPR_KIND_WINDOW_FRAME_GROUPS,	/* window frame clause with GROUPS */
 	EXPR_KIND_SELECT_TARGET,	/* SELECT target list item */
 	EXPR_KIND_INSERT_TARGET,	/* INSERT target list item */
 	EXPR_KIND_UPDATE_SOURCE,	/* UPDATE assignment source item */
diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out
index d8ae3807a2..b8220d44d2 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -858,6 +858,23 @@ FROM tenk1 WHERE unique1 < 10;
   10 |       0 |    0
 (10 rows)
 
+SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude group),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+     |       4 |    0
+     |       2 |    2
+     |       1 |    1
+     |       6 |    2
+     |       9 |    1
+     |       8 |    0
+     |       5 |    1
+     |       3 |    3
+     |       7 |    3
+     |       0 |    0
+(10 rows)
+
 SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude ties),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
@@ -960,6 +977,23 @@ FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four);
   38 |       7 |    3
 (10 rows)
 
+SELECT sum(unique1) over (w range between unbounded preceding and current row exclude group),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four);
+ sum | unique1 | four 
+-----+---------+------
+     |       0 |    0
+     |       8 |    0
+     |       4 |    0
+  12 |       5 |    1
+  12 |       9 |    1
+  12 |       1 |    1
+  27 |       6 |    2
+  27 |       2 |    2
+  35 |       3 |    3
+  35 |       7 |    3
+(10 rows)
+
 SELECT sum(unique1) over (w range between unbounded preceding and current row exclude ties),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four);
@@ -1069,6 +1103,32 @@ SELECT pg_get_viewdef('v_window');
 
 CREATE OR REPLACE TEMP VIEW v_window AS
 	SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following
+	exclude group) as sum_rows FROM generate_series(1, 10) i;
+SELECT * FROM v_window;
+ i  | sum_rows 
+----+----------
+  1 |        2
+  2 |        4
+  3 |        6
+  4 |        8
+  5 |       10
+  6 |       12
+  7 |       14
+  8 |       16
+  9 |       18
+ 10 |        9
+(10 rows)
+
+SELECT pg_get_viewdef('v_window');
+                                           pg_get_viewdef                                            
+-----------------------------------------------------------------------------------------------------
+  SELECT i.i,                                                                                       +
+     sum(i.i) OVER (ORDER BY i.i ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE GROUP) AS sum_rows+
+    FROM generate_series(1, 10) i(i);
+(1 row)
+
+CREATE OR REPLACE TEMP VIEW v_window AS
+	SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following
 	exclude ties) as sum_rows FROM generate_series(1, 10) i;
 SELECT * FROM v_window;
  i  | sum_rows 
@@ -1119,6 +1179,32 @@ SELECT pg_get_viewdef('v_window');
     FROM generate_series(1, 10) i(i);
 (1 row)
 
+CREATE OR REPLACE TEMP VIEW v_window AS
+	SELECT i, sum(i) over (order by i groups between 1 preceding and 1 following
+	exclude no others) as sum_rows FROM generate_series(1, 10) i;
+SELECT * FROM v_window;
+ i  | sum_rows 
+----+----------
+  1 |        3
+  2 |        6
+  3 |        9
+  4 |       12
+  5 |       15
+  6 |       18
+  7 |       21
+  8 |       24
+  9 |       27
+ 10 |       19
+(10 rows)
+
+SELECT pg_get_viewdef('v_window');
+                                              pg_get_viewdef                                               
+-----------------------------------------------------------------------------------------------------------
+  SELECT i.i,                                                                                             +
+     sum(i.i) OVER (ORDER BY i.i GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE NO OTHERS) AS sum_rows+
+    FROM generate_series(1, 10) i(i);
+(1 row)
+
 -- RANGE BETWEEN with values tests
 SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding),
 	unique1, four
@@ -1171,6 +1257,23 @@ FROM tenk1 WHERE unique1 < 10;
   23 |       7 |    3
 (10 rows)
 
+SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding exclude group),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+     |       0 |    0
+     |       8 |    0
+     |       4 |    0
+  12 |       5 |    1
+  12 |       9 |    1
+  12 |       1 |    1
+  27 |       6 |    2
+  27 |       2 |    2
+  23 |       3 |    3
+  23 |       7 |    3
+(10 rows)
+
 SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding exclude ties),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
@@ -1205,6 +1308,23 @@ FROM tenk1 WHERE unique1 < 10;
   30 |       7 |    3
 (10 rows)
 
+SELECT sum(unique1) over (order by four range between 2::int8 preceding and 6::int2 following exclude group),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  33 |       0 |    0
+  33 |       8 |    0
+  33 |       4 |    0
+  30 |       5 |    1
+  30 |       9 |    1
+  30 |       1 |    1
+  37 |       6 |    2
+  37 |       2 |    2
+  23 |       3 |    3
+  23 |       7 |    3
+(10 rows)
+
 SELECT sum(unique1) over (partition by four order by unique1 range between 5::int8 preceding and 6::int2 following),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
@@ -1272,6 +1392,22 @@ select sum(salary) over (order by enroll_date range between '1 year'::interval p
 (10 rows)
 
 select sum(salary) over (order by enroll_date range between '1 year'::interval preceding and '1 year'::interval following
+	exclude group), salary, enroll_date from empsalary;
+  sum  | salary | enroll_date 
+-------+--------+-------------
+ 23900 |   5000 | 10-01-2006
+ 23900 |   6000 | 10-01-2006
+ 34500 |   3900 | 12-23-2006
+ 37100 |   4800 | 08-01-2007
+ 37100 |   5200 | 08-01-2007
+ 42300 |   4800 | 08-08-2007
+ 41900 |   5200 | 08-15-2007
+ 32600 |   3500 | 12-10-2007
+ 23500 |   4500 | 01-01-2008
+ 23500 |   4200 | 01-01-2008
+(10 rows)
+
+select sum(salary) over (order by enroll_date range between '1 year'::interval preceding and '1 year'::interval following
 	exclude ties), salary, enroll_date from empsalary;
   sum  | salary | enroll_date 
 -------+--------+-------------
@@ -1320,6 +1456,22 @@ select sum(salary) over (order by enroll_time range between '1 hour'::interval p
 (10 rows)
 
 select sum(salary) over (order by enroll_time range between '1 hour'::interval preceding and '2 hours'::interval following
+	exclude group), salary, enroll_time from empsalary;
+  sum  | salary | enroll_time 
+-------+--------+-------------
+  8500 |   5200 | 11:00:00
+ 13500 |   5000 | 12:00:00
+ 17900 |   3500 | 13:00:00
+ 11600 |   4800 | 14:00:00
+  9300 |   3900 | 15:00:00
+  9300 |   4200 | 15:00:00
+ 10800 |   4500 | 17:00:00
+ 15700 |   4800 | 18:00:00
+ 10000 |   6000 | 19:00:00
+  6000 |   5200 | 20:00:00
+(10 rows)
+
+select sum(salary) over (order by enroll_time range between '1 hour'::interval preceding and '2 hours'::interval following
 	exclude ties), salary, enroll_time from empsalary;
   sum  | salary | enroll_time 
 -------+--------+-------------
@@ -1368,6 +1520,22 @@ select sum(salary) over (order by enroll_timetz range between '1 hour'::interval
 (10 rows)
 
 select sum(salary) over (order by enroll_timetz range between '1 hour'::interval preceding and '2 hours'::interval following
+	exclude group), salary, enroll_timetz from empsalary;
+  sum  | salary | enroll_timetz 
+-------+--------+---------------
+  8500 |   5200 | 11:00:00+01
+ 13500 |   5000 | 12:00:00+01
+ 17900 |   3500 | 13:00:00+01
+ 11600 |   4800 | 14:00:00+01
+  9300 |   3900 | 15:00:00+01
+  9300 |   4200 | 15:00:00+01
+ 10800 |   4500 | 17:00:00+01
+ 15700 |   4800 | 18:00:00+01
+ 10000 |   6000 | 19:00:00+01
+  6000 |   5200 | 20:00:00+01
+(10 rows)
+
+select sum(salary) over (order by enroll_timetz range between '1 hour'::interval preceding and '2 hours'::interval following
 	exclude ties), salary, enroll_timetz from empsalary;
   sum  | salary | enroll_timetz 
 -------+--------+---------------
@@ -1416,6 +1584,22 @@ select sum(salary) over (order by enroll_interval range between '1 year'::interv
 (10 rows)
 
 select sum(salary) over (order by enroll_interval range between '1 year'::interval preceding and '2 years'::interval following
+	exclude group), salary, enroll_interval from empsalary;
+  sum  | salary | enroll_interval 
+-------+--------+-----------------
+  8500 |   5200 | @ 1 year
+ 13500 |   5000 | @ 2 years
+ 17900 |   3500 | @ 3 years
+ 11600 |   4800 | @ 4 years
+  9300 |   3900 | @ 5 years
+  9300 |   4200 | @ 5 years
+ 10800 |   4500 | @ 7 years
+ 15700 |   4800 | @ 8 years
+ 10000 |   6000 | @ 9 years
+  6000 |   5200 | @ 10 years
+(10 rows)
+
+select sum(salary) over (order by enroll_interval range between '1 year'::interval preceding and '2 years'::interval following
 	exclude ties), salary, enroll_interval from empsalary;
   sum  | salary | enroll_interval 
 -------+--------+-----------------
@@ -1464,6 +1648,22 @@ select sum(salary) over (order by enroll_timestamptz range between '1 year'::int
 (10 rows)
 
 select sum(salary) over (order by enroll_timestamptz range between '1 year'::interval preceding and '2 years'::interval following
+	exclude group), salary, enroll_timestamptz from empsalary;
+  sum  | salary |      enroll_timestamptz      
+-------+--------+------------------------------
+ 13300 |   5200 | Thu Oct 19 10:23:54 2000 PDT
+ 13900 |   5000 | Fri Oct 19 10:23:54 2001 PDT
+ 13900 |   3500 | Fri Oct 19 10:23:54 2001 PDT
+ 16600 |   4800 | Sat Oct 19 10:23:54 2002 PDT
+ 13500 |   3900 | Sun Oct 19 10:23:54 2003 PDT
+ 13200 |   4200 | Tue Oct 19 10:23:54 2004 PDT
+ 15000 |   4500 | Wed Oct 19 10:23:54 2005 PDT
+ 15700 |   4800 | Thu Oct 19 10:23:54 2006 PDT
+ 10000 |   6000 | Fri Oct 19 10:23:54 2007 PDT
+  6000 |   5200 | Sun Oct 19 10:23:54 2008 PDT
+(10 rows)
+
+select sum(salary) over (order by enroll_timestamptz range between '1 year'::interval preceding and '2 years'::interval following
 	exclude ties), salary, enroll_timestamptz from empsalary;
   sum  | salary |      enroll_timestamptz      
 -------+--------+------------------------------
@@ -1512,6 +1712,22 @@ select sum(salary) over (order by enroll_timestamp range between '1 year'::inter
 (10 rows)
 
 select sum(salary) over (order by enroll_timestamp range between '1 year'::interval preceding and '2 years'::interval following
+	exclude group), salary, enroll_timestamp from empsalary;
+  sum  | salary |     enroll_timestamp     
+-------+--------+--------------------------
+ 13300 |   5200 | Thu Oct 19 10:23:54 2000
+ 13900 |   5000 | Fri Oct 19 10:23:54 2001
+ 13900 |   3500 | Fri Oct 19 10:23:54 2001
+ 16600 |   4800 | Sat Oct 19 10:23:54 2002
+ 13500 |   3900 | Sun Oct 19 10:23:54 2003
+ 13200 |   4200 | Tue Oct 19 10:23:54 2004
+ 15000 |   4500 | Wed Oct 19 10:23:54 2005
+ 15700 |   4800 | Thu Oct 19 10:23:54 2006
+ 10000 |   6000 | Fri Oct 19 10:23:54 2007
+  6000 |   5200 | Sun Oct 19 10:23:54 2008
+(10 rows)
+
+select sum(salary) over (order by enroll_timestamp range between '1 year'::interval preceding and '2 years'::interval following
 	exclude ties), salary, enroll_timestamp from empsalary;
   sum  | salary |     enroll_timestamp     
 -------+--------+--------------------------
@@ -1592,6 +1808,22 @@ select sum(salary) over(order by enroll_date range between unbounded preceding a
 (10 rows)
 
 select sum(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following
+	exclude group), salary, enroll_date from empsalary;
+  sum  | salary | enroll_date 
+-------+--------+-------------
+ 23900 |   5000 | 10-01-2006
+ 23900 |   6000 | 10-01-2006
+ 34500 |   3900 | 12-23-2006
+ 37100 |   4800 | 08-01-2007
+ 37100 |   5200 | 08-01-2007
+ 42300 |   4800 | 08-08-2007
+ 41900 |   5200 | 08-15-2007
+ 43600 |   3500 | 12-10-2007
+ 38400 |   4500 | 01-01-2008
+ 38400 |   4200 | 01-01-2008
+(10 rows)
+
+select sum(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following
 	exclude ties), salary, enroll_date from empsalary;
   sum  | salary | enroll_date 
 -------+--------+-------------
@@ -1637,6 +1869,475 @@ select max(enroll_date) over (order by salary range between '1 year'::interval p
 ERROR:  Offsets must be an integral
 LINE 1: select max(enroll_date) over (order by salary range between ...
                                      ^
+-- GROUPS tests
+SELECT sum(unique1) over (order by four groups between unbounded preceding and current row),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  12 |       0 |    0
+  12 |       8 |    0
+  12 |       4 |    0
+  27 |       5 |    1
+  27 |       9 |    1
+  27 |       1 |    1
+  35 |       6 |    2
+  35 |       2 |    2
+  45 |       3 |    3
+  45 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between unbounded preceding and unbounded following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  45 |       0 |    0
+  45 |       8 |    0
+  45 |       4 |    0
+  45 |       5 |    1
+  45 |       9 |    1
+  45 |       1 |    1
+  45 |       6 |    2
+  45 |       2 |    2
+  45 |       3 |    3
+  45 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between current row and unbounded following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  45 |       0 |    0
+  45 |       8 |    0
+  45 |       4 |    0
+  33 |       5 |    1
+  33 |       9 |    1
+  33 |       1 |    1
+  18 |       6 |    2
+  18 |       2 |    2
+  10 |       3 |    3
+  10 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between 1 preceding and unbounded following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  45 |       0 |    0
+  45 |       8 |    0
+  45 |       4 |    0
+  45 |       5 |    1
+  45 |       9 |    1
+  45 |       1 |    1
+  33 |       6 |    2
+  33 |       2 |    2
+  18 |       3 |    3
+  18 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between 1 following and unbounded following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  33 |       0 |    0
+  33 |       8 |    0
+  33 |       4 |    0
+  18 |       5 |    1
+  18 |       9 |    1
+  18 |       1 |    1
+  10 |       6 |    2
+  10 |       2 |    2
+     |       3 |    3
+     |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between unbounded preceding and 2 following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  35 |       0 |    0
+  35 |       8 |    0
+  35 |       4 |    0
+  45 |       5 |    1
+  45 |       9 |    1
+  45 |       1 |    1
+  45 |       6 |    2
+  45 |       2 |    2
+  45 |       3 |    3
+  45 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 preceding),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+     |       0 |    0
+     |       8 |    0
+     |       4 |    0
+  12 |       5 |    1
+  12 |       9 |    1
+  12 |       1 |    1
+  27 |       6 |    2
+  27 |       2 |    2
+  23 |       3 |    3
+  23 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  27 |       0 |    0
+  27 |       8 |    0
+  27 |       4 |    0
+  35 |       5 |    1
+  35 |       9 |    1
+  35 |       1 |    1
+  45 |       6 |    2
+  45 |       2 |    2
+  33 |       3 |    3
+  33 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between 0 preceding and 0 following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  12 |       0 |    0
+  12 |       8 |    0
+  12 |       4 |    0
+  15 |       5 |    1
+  15 |       9 |    1
+  15 |       1 |    1
+   8 |       6 |    2
+   8 |       2 |    2
+  10 |       3 |    3
+  10 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following
+	exclude current row), unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  27 |       0 |    0
+  19 |       8 |    0
+  23 |       4 |    0
+  30 |       5 |    1
+  26 |       9 |    1
+  34 |       1 |    1
+  39 |       6 |    2
+  43 |       2 |    2
+  30 |       3 |    3
+  26 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following
+	exclude group), unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  15 |       0 |    0
+  15 |       8 |    0
+  15 |       4 |    0
+  20 |       5 |    1
+  20 |       9 |    1
+  20 |       1 |    1
+  37 |       6 |    2
+  37 |       2 |    2
+  23 |       3 |    3
+  23 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following
+	exclude ties), unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four 
+-----+---------+------
+  15 |       0 |    0
+  23 |       8 |    0
+  19 |       4 |    0
+  25 |       5 |    1
+  29 |       9 |    1
+  21 |       1 |    1
+  43 |       6 |    2
+  39 |       2 |    2
+  26 |       3 |    3
+  30 |       7 |    3
+(10 rows)
+
+SELECT sum(unique1) over (partition by ten
+	order by four groups between 0 preceding and 0 following),unique1, four, ten
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four | ten 
+-----+---------+------+-----
+   0 |       0 |    0 |   0
+   1 |       1 |    1 |   1
+   2 |       2 |    2 |   2
+   3 |       3 |    3 |   3
+   4 |       4 |    0 |   4
+   5 |       5 |    1 |   5
+   6 |       6 |    2 |   6
+   7 |       7 |    3 |   7
+   8 |       8 |    0 |   8
+   9 |       9 |    1 |   9
+(10 rows)
+
+SELECT sum(unique1) over (partition by ten
+	order by four groups between 0 preceding and 0 following exclude current row), unique1, four, ten
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four | ten 
+-----+---------+------+-----
+     |       0 |    0 |   0
+     |       1 |    1 |   1
+     |       2 |    2 |   2
+     |       3 |    3 |   3
+     |       4 |    0 |   4
+     |       5 |    1 |   5
+     |       6 |    2 |   6
+     |       7 |    3 |   7
+     |       8 |    0 |   8
+     |       9 |    1 |   9
+(10 rows)
+
+SELECT sum(unique1) over (partition by ten
+	order by four groups between 0 preceding and 0 following exclude group), unique1, four, ten
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four | ten 
+-----+---------+------+-----
+     |       0 |    0 |   0
+     |       1 |    1 |   1
+     |       2 |    2 |   2
+     |       3 |    3 |   3
+     |       4 |    0 |   4
+     |       5 |    1 |   5
+     |       6 |    2 |   6
+     |       7 |    3 |   7
+     |       8 |    0 |   8
+     |       9 |    1 |   9
+(10 rows)
+
+SELECT sum(unique1) over (partition by ten
+	order by four groups between 0 preceding and 0 following exclude ties), unique1, four, ten
+FROM tenk1 WHERE unique1 < 10;
+ sum | unique1 | four | ten 
+-----+---------+------+-----
+   0 |       0 |    0 |   0
+   1 |       1 |    1 |   1
+   2 |       2 |    2 |   2
+   3 |       3 |    3 |   3
+   4 |       4 |    0 |   4
+   5 |       5 |    1 |   5
+   6 |       6 |    2 |   6
+   7 |       7 |    3 |   7
+   8 |       8 |    0 |   8
+   9 |       9 |    1 |   9
+(10 rows)
+
+-- Show differences in values mode between ROWS, RANGE, and GROUPS
+WITH cte (x) AS (
+        SELECT * FROM generate_series(1, 35, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x rows between 1 preceding and 1 following);
+ x  | sum 
+----+-----
+  1 |   4
+  3 |   9
+  5 |  15
+  7 |  21
+  9 |  27
+ 11 |  33
+ 13 |  39
+ 15 |  45
+ 17 |  51
+ 19 |  57
+ 21 |  63
+ 23 |  69
+ 25 |  75
+ 27 |  81
+ 29 |  87
+ 31 |  93
+ 33 |  99
+ 35 |  68
+(18 rows)
+
+WITH cte (x) AS (
+        SELECT * FROM generate_series(1, 35, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x range between 1 preceding and 1 following);
+ x  | sum 
+----+-----
+  1 |   1
+  3 |   3
+  5 |   5
+  7 |   7
+  9 |   9
+ 11 |  11
+ 13 |  13
+ 15 |  15
+ 17 |  17
+ 19 |  19
+ 21 |  21
+ 23 |  23
+ 25 |  25
+ 27 |  27
+ 29 |  29
+ 31 |  31
+ 33 |  33
+ 35 |  35
+(18 rows)
+
+WITH cte (x) AS (
+        SELECT * FROM generate_series(1, 35, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x groups between 1 preceding and 1 following);
+ x  | sum 
+----+-----
+  1 |   4
+  3 |   9
+  5 |  15
+  7 |  21
+  9 |  27
+ 11 |  33
+ 13 |  39
+ 15 |  45
+ 17 |  51
+ 19 |  57
+ 21 |  63
+ 23 |  69
+ 25 |  75
+ 27 |  81
+ 29 |  87
+ 31 |  93
+ 33 |  99
+ 35 |  68
+(18 rows)
+
+WITH cte (x) AS (
+        select 1 union all select 1 union all select 1 union all
+        SELECT * FROM generate_series(5, 49, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x rows between 1 preceding and 1 following);
+ x  | sum 
+----+-----
+  1 |   2
+  1 |   3
+  1 |   7
+  5 |  13
+  7 |  21
+  9 |  27
+ 11 |  33
+ 13 |  39
+ 15 |  45
+ 17 |  51
+ 19 |  57
+ 21 |  63
+ 23 |  69
+ 25 |  75
+ 27 |  81
+ 29 |  87
+ 31 |  93
+ 33 |  99
+ 35 | 105
+ 37 | 111
+ 39 | 117
+ 41 | 123
+ 43 | 129
+ 45 | 135
+ 47 | 141
+ 49 |  96
+(26 rows)
+
+WITH cte (x) AS (
+        select 1 union all select 1 union all select 1 union all
+        SELECT * FROM generate_series(5, 49, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x range between 1 preceding and 1 following);
+ x  | sum 
+----+-----
+  1 |   3
+  1 |   3
+  1 |   3
+  5 |   5
+  7 |   7
+  9 |   9
+ 11 |  11
+ 13 |  13
+ 15 |  15
+ 17 |  17
+ 19 |  19
+ 21 |  21
+ 23 |  23
+ 25 |  25
+ 27 |  27
+ 29 |  29
+ 31 |  31
+ 33 |  33
+ 35 |  35
+ 37 |  37
+ 39 |  39
+ 41 |  41
+ 43 |  43
+ 45 |  45
+ 47 |  47
+ 49 |  49
+(26 rows)
+
+WITH cte (x) AS (
+        select 1 union all select 1 union all select 1 union all
+        SELECT * FROM generate_series(5, 49, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x groups between 1 preceding and 1 following);
+ x  | sum 
+----+-----
+  1 |   8
+  1 |   8
+  1 |   8
+  5 |  15
+  7 |  21
+  9 |  27
+ 11 |  33
+ 13 |  39
+ 15 |  45
+ 17 |  51
+ 19 |  57
+ 21 |  63
+ 23 |  69
+ 25 |  75
+ 27 |  81
+ 29 |  87
+ 31 |  93
+ 33 |  99
+ 35 | 105
+ 37 | 111
+ 39 | 117
+ 41 | 123
+ 43 | 129
+ 45 | 135
+ 47 | 141
+ 49 |  96
+(26 rows)
+
 -- with UNION
 SELECT count(*) OVER (PARTITION BY four) FROM (SELECT * FROM tenk1 UNION ALL SELECT * FROM tenk2)s LIMIT 0;
  count 
diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql
index cd25200501..87346fcf0f 100644
--- a/src/test/regress/sql/window.sql
+++ b/src/test/regress/sql/window.sql
@@ -202,6 +202,10 @@ SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude curre
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
 
+SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude group),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
 SELECT sum(unique1) over (rows between 2 preceding and 2 following exclude ties),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
@@ -226,6 +230,10 @@ SELECT sum(unique1) over (w range between unbounded preceding and current row ex
 	unique1, four
 FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four);
 
+SELECT sum(unique1) over (w range between unbounded preceding and current row exclude group),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four);
+
 SELECT sum(unique1) over (w range between unbounded preceding and current row exclude ties),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10 WINDOW w AS (order by four);
@@ -260,6 +268,14 @@ SELECT pg_get_viewdef('v_window');
 
 CREATE OR REPLACE TEMP VIEW v_window AS
 	SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following
+	exclude group) as sum_rows FROM generate_series(1, 10) i;
+
+SELECT * FROM v_window;
+
+SELECT pg_get_viewdef('v_window');
+
+CREATE OR REPLACE TEMP VIEW v_window AS
+	SELECT i, sum(i) over (order by i rows between 1 preceding and 1 following
 	exclude ties) as sum_rows FROM generate_series(1, 10) i;
 
 SELECT * FROM v_window;
@@ -274,6 +290,14 @@ SELECT * FROM v_window;
 
 SELECT pg_get_viewdef('v_window');
 
+CREATE OR REPLACE TEMP VIEW v_window AS
+	SELECT i, sum(i) over (order by i groups between 1 preceding and 1 following
+	exclude no others) as sum_rows FROM generate_series(1, 10) i;
+
+SELECT * FROM v_window;
+
+SELECT pg_get_viewdef('v_window');
+
 -- RANGE BETWEEN with values tests
 SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding),
 	unique1, four
@@ -287,6 +311,10 @@ SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::i
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
 
+SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding exclude group),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
 SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding exclude ties),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
@@ -295,6 +323,10 @@ SELECT sum(unique1) over (order by four range between 2::int8 preceding and 6::i
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
 
+SELECT sum(unique1) over (order by four range between 2::int8 preceding and 6::int2 following exclude group),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
 SELECT sum(unique1) over (partition by four order by unique1 range between 5::int8 preceding and 6::int2 following),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
@@ -310,6 +342,9 @@ select sum(salary) over (order by enroll_date range between '1 year'::interval p
 	exclude current row), salary, enroll_date from empsalary;
 
 select sum(salary) over (order by enroll_date range between '1 year'::interval preceding and '1 year'::interval following
+	exclude group), salary, enroll_date from empsalary;
+
+select sum(salary) over (order by enroll_date range between '1 year'::interval preceding and '1 year'::interval following
 	exclude ties), salary, enroll_date from empsalary;
 
 select sum(salary) over (order by enroll_time range between '1 hour'::interval preceding and '2 hours'::interval following),
@@ -319,6 +354,9 @@ select sum(salary) over (order by enroll_time range between '1 hour'::interval p
 	exclude current row), salary, enroll_time from empsalary;
 
 select sum(salary) over (order by enroll_time range between '1 hour'::interval preceding and '2 hours'::interval following
+	exclude group), salary, enroll_time from empsalary;
+
+select sum(salary) over (order by enroll_time range between '1 hour'::interval preceding and '2 hours'::interval following
 	exclude ties), salary, enroll_time from empsalary;
 
 select sum(salary) over (order by enroll_timetz range between '1 hour'::interval preceding and '2 hours'::interval following),
@@ -328,6 +366,9 @@ select sum(salary) over (order by enroll_timetz range between '1 hour'::interval
 	exclude current row), salary, enroll_timetz from empsalary;
 
 select sum(salary) over (order by enroll_timetz range between '1 hour'::interval preceding and '2 hours'::interval following
+	exclude group), salary, enroll_timetz from empsalary;
+
+select sum(salary) over (order by enroll_timetz range between '1 hour'::interval preceding and '2 hours'::interval following
 	exclude ties), salary, enroll_timetz from empsalary;
 
 select sum(salary) over (order by enroll_interval range between '1 year'::interval preceding and '2 years'::interval following),
@@ -337,6 +378,9 @@ select sum(salary) over (order by enroll_interval range between '1 year'::interv
 	exclude current row), salary, enroll_interval from empsalary;
 
 select sum(salary) over (order by enroll_interval range between '1 year'::interval preceding and '2 years'::interval following
+	exclude group), salary, enroll_interval from empsalary;
+
+select sum(salary) over (order by enroll_interval range between '1 year'::interval preceding and '2 years'::interval following
 	exclude ties), salary, enroll_interval from empsalary;
 
 select sum(salary) over (order by enroll_timestamptz range between '1 year'::interval preceding and '2 years'::interval following),
@@ -346,6 +390,9 @@ select sum(salary) over (order by enroll_timestamptz range between '1 year'::int
 	exclude current row), salary, enroll_timestamptz from empsalary;
 
 select sum(salary) over (order by enroll_timestamptz range between '1 year'::interval preceding and '2 years'::interval following
+	exclude group), salary, enroll_timestamptz from empsalary;
+
+select sum(salary) over (order by enroll_timestamptz range between '1 year'::interval preceding and '2 years'::interval following
 	exclude ties), salary, enroll_timestamptz from empsalary;
 
 select sum(salary) over (order by enroll_timestamp range between '1 year'::interval preceding and '2 years'::interval following),
@@ -355,6 +402,9 @@ select sum(salary) over (order by enroll_timestamp range between '1 year'::inter
 	exclude current row), salary, enroll_timestamp from empsalary;
 
 select sum(salary) over (order by enroll_timestamp range between '1 year'::interval preceding and '2 years'::interval following
+	exclude group), salary, enroll_timestamp from empsalary;
+
+select sum(salary) over (order by enroll_timestamp range between '1 year'::interval preceding and '2 years'::interval following
 	exclude ties), salary, enroll_timestamp from empsalary;
 
 select sum(salary) over (order by enroll_timestamp range between current row and '2 years'::interval following),
@@ -370,6 +420,9 @@ select sum(salary) over(order by enroll_date range between unbounded preceding a
 	exclude current row), salary, enroll_date from empsalary;
 
 select sum(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following
+	exclude group), salary, enroll_date from empsalary;
+
+select sum(salary) over(order by enroll_date range between unbounded preceding and '1 year'::interval following
 	exclude ties), salary, enroll_date from empsalary;
 
 -- RANGE BETWEEN with values negative tests
@@ -394,6 +447,118 @@ select max(enroll_date) over (order by salary range between 1 preceding and -2 f
 select max(enroll_date) over (order by salary range between '1 year'::interval preceding and '2 years'::interval following
 	exclude ties), salary, enroll_timestamp from empsalary;
 
+-- GROUPS tests
+
+SELECT sum(unique1) over (order by four groups between unbounded preceding and current row),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between unbounded preceding and unbounded following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between current row and unbounded following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between 1 preceding and unbounded following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between 1 following and unbounded following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between unbounded preceding and 2 following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 preceding),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between 0 preceding and 0 following),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following
+	exclude current row), unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following
+	exclude group), unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (order by four groups between 2 preceding and 1 following
+	exclude ties), unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (partition by ten
+	order by four groups between 0 preceding and 0 following),unique1, four, ten
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (partition by ten
+	order by four groups between 0 preceding and 0 following exclude current row), unique1, four, ten
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (partition by ten
+	order by four groups between 0 preceding and 0 following exclude group), unique1, four, ten
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT sum(unique1) over (partition by ten
+	order by four groups between 0 preceding and 0 following exclude ties), unique1, four, ten
+FROM tenk1 WHERE unique1 < 10;
+
+-- Show differences in values mode between ROWS, RANGE, and GROUPS
+WITH cte (x) AS (
+        SELECT * FROM generate_series(1, 35, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x rows between 1 preceding and 1 following);
+
+WITH cte (x) AS (
+        SELECT * FROM generate_series(1, 35, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x range between 1 preceding and 1 following);
+
+WITH cte (x) AS (
+        SELECT * FROM generate_series(1, 35, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x groups between 1 preceding and 1 following);
+
+WITH cte (x) AS (
+        select 1 union all select 1 union all select 1 union all
+        SELECT * FROM generate_series(5, 49, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x rows between 1 preceding and 1 following);
+
+WITH cte (x) AS (
+        select 1 union all select 1 union all select 1 union all
+        SELECT * FROM generate_series(5, 49, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x range between 1 preceding and 1 following);
+
+WITH cte (x) AS (
+        select 1 union all select 1 union all select 1 union all
+        SELECT * FROM generate_series(5, 49, 2)
+)
+SELECT x, (sum(x) over w)
+FROM cte
+WINDOW w AS (ORDER BY x groups between 1 preceding and 1 following);
+
 -- with UNION
 SELECT count(*) OVER (PARTITION BY four) FROM (SELECT * FROM tenk1 UNION ALL SELECT * FROM tenk2)s LIMIT 0;
 
