[PATCH] Covering SPGiST index

Started by Pavel Borisovover 5 years ago31 messages
#1Pavel Borisov
pashkin.elfe@gmail.com
1 attachment(s)

Hi, hackers!

I'd like to propose a patch which introduces a functionality to include
additional columns to SPGiST index to increase speed of queries containing
them due to making the scans index only in this case. To date this
functionality was available in GiSt and btree, I suppose the same is useful
in SPGiST also.

A few words on realisaton:

1. The patch is intended to be fully compatible with previous SPGiSt
indexes so SpGist leaf tuple structure remains unchanged until the ending
of key attribute. All changes are introduced only after it. Internal tuples
remain unchanged at all.

2. Included data is added in the form very similar to heap tuple but unlike
the later it should not start from MAXALIGN boundary. I.e. nulls mask (if
exist) starts just after the key value (it doesn't need alignment). Each of
included attributes start from their own typealign boundary. The goal is to
make leaf tuples and therefore index more compact.

3. Leaf tuple header is modified to store additional per tuple flags:
a) is nullmask present - if there is at least one null value among included
attributes of a tuple
(Note that this nullmask apply only to include attributes as nulls
management for key attributes is already realised in SPGiSt by placing
leafs with null keys in separate list not in the main index tree.)
b) is there variable length values among included. If there is no and key
attribute is also fixed-length e.g. (kd-tree, quad-tree etc.) then leaf
tuple processing can be speed up using attcacheoff.

These bits are incorporated into unused higher bits of nextOffset in the
header SPGiST leaf tuple. Even if we have 64Kb pages and tuples of minimum
12 bytes (the length of the header on 32-bit architectures) + 4 bytes
ItemIdData 14 bit for nextOffset is more than enough.

All this changes only affect private index structures so all outside
behavior like WAL, vacuum etc will remain unchanged.

As usual I very much appreciate your feedback

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

spgist-covering-0001.diffapplication/octet-stream; name=spgist-covering-0001.diffDownload
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..b767a805fa 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET (leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset,InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset,r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum  		*leafChainDatums;
+	bool 	 	*leafChainIsnulls;
+	const int natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n*natts*sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n*natts*sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,14 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in a chain are collected
+	 * into 2-d arrays: (number of tuples in chain) x (number of attributes)
+	 * First attribute is key, the other - included attributes (if any). After
+	 * picksplit we need to form new leaf tuples as key attribute length can
+	 * change which can affect alignment of every include attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +771,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				SpGistDeformLeafTuple(it, state, leafChainDatums+nToInsert*natts,
+									  leafChainIsnulls+nToInsert*natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +798,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				SpGistDeformLeafTuple(it, state, leafChainDatums+nToInsert*natts,
+									  leafChainIsnulls+nToInsert*natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +812,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +820,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +833,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	SpGistDeformLeafTuple(newLeafTuple, state, leafChainDatums+(in.nTuples)*natts,
+						  leafChainIsnulls+(in.nTuples)*natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values are used
+	 * to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +861,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums+i*natts)=(Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls+i*natts)=false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums+i*natts,
+										   leafChainIsnulls+i*natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +886,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums+i*natts)=(Datum) 0;
+			*(leafChainIsnulls+i*natts)=true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums+i*natts,
+										   leafChainIsnulls+i*natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1227,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1920,81 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum		*leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
-	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/* If an optional "compress" method is provided, then call it to form the
+	 * key datum from the input datum.  Otherwise store the input datum as
 	 * is.  Since we don't use index_form_tuple in this AM, we have to make
 	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
 	 * guarantee that.  But we assume the "compress" method to return an
 	 * untoasted value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
 										  index->rd_indcollation[0],
-										  datum);
+										  datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+		{
+			if (!isnull[i])
+			{
+				if (TupleDescAttr(state->includeTupdesc, i-1)->attlen == -1)
+					leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+				else
+					leafDatum[i]=datum[i];
+			}
+			else
+				leafDatum[i]=(Datum) 0;
+		}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = SpgLeafSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2006,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2040,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2082,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2099,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2113,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2155,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2166,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2203,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = SpgLeafSize(state, leafDatum, isnull) +
+								   sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2272,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..b5dedc3afd 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,29 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
+	/*
+	 * If there are include attributes search item in the queue should
+	 * contain them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
 
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +524,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,13 +551,12 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
 														isnull,
 														distances);
-
 			spgAddSearchItemToQueue(so, heapItem);
 
 			MemoryContextSwitchTo(oldCxt);
@@ -543,8 +565,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+							 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +760,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +774,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +806,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +901,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +928,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -923,7 +947,7 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 
 			for (i = 0; i < so->numberOfOrderBys; i++)
 			{
-				int			offset = so->nonNullOrderByOffsets[i];
+				int		offset = so->nonNullOrderByOffsets[i];
 
 				if (offset >= 0)
 				{
@@ -949,9 +973,35 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+		if (so->state.includeTupdesc)
+		{
+			/* Add included attributes */
+			Datum *leafDatums;
+			bool *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			SpGistDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/* override key value extracted from LeafTuple in case we've reconstructed it already */
+			leafDatums[0]=leafValue;
+			leafIsnulls[0]=isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+												   leafDatums,
+												   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
 												   &leafValue,
 												   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1018,6 +1068,9 @@ bool
 spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
+	
+	/* Included attributes always can be fetched for index-only scans */
+	if (attno > 1) return true;
 
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..9e8ebc9f87 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,8 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
-
-
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
+
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								  bool *isnull, Size start);
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
  * and callbacks.
@@ -49,7 +59,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +67,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -112,18 +122,17 @@ spgGetCache(Relation index)
 		FmgrInfo   *procinfo;
 		Buffer		metabuffer;
 		SpGistMetaPageData *metadata;
-
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
-
+		/* SPGiST should have one key column and can also have included columns */
+		Assert(IndexRelationGetNumberOfKeyAttributes(index) == 1);
 		/*
-		 * Get the actual data type of the indexed column from the index
+		 * Get the actual data type of the key column from the index
 		 * tupdesc.  We pass this to the opclass config function so that
 		 * polymorphic opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +165,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +187,22 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+		/* Form descriptor for included columns if any */
+		if (IndexRelationGetNumberOfAttributes(index) > 1)
+		{
+			int i;
+			cache->includeTupdesc = CreateTemplateTupleDesc(
+							IndexRelationGetNumberOfAttributes(index) - 1);
 
+			for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+			{
+				TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+								   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+								   -1, 0);
+			}
+		}
+		else
+			cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +215,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,7 +629,7 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
+ * Note the result is not maxaligned and this should be done by caller if needed.
  * Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
@@ -619,7 +645,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +668,198 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * necessarily MAXALIGNed. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of included
+ * data area in SpGiST leaf tuple.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					   Datum *values,
+					   bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length-start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed only for backward
+ * compatibility and there might be gap between header and key data. After key
+ * data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed.*/
+unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for include attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_COLUMNS),
+				 errmsg("number of index columns (%d) exceeds limit (%d)",
+						state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall included attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum+1, isnull+1, size);
+	}
+return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and included
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to included attributes and is placed just after key data if
+ * there is at least one NULL among included attributes. It doesn't need alignment.
+ * Then all included columns data follow aligned by their typealign's.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size=SGLTHDRSZ;
+	unsigned int include_offset=0;
+	unsigned int nullmask_size=0;
+	unsigned int data_offset=0;
+	unsigned int data_size=0;
+	uint16 tupmask=0;
+	int i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are include attributes also calculate sizes and
+	 * offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_COLUMNS),
+				 errmsg("number of index columns (%d) exceeds limit (%d)",
+						state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all included attributes is counted inside data_size. data_offset
+		 * itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum+1, isnull+1, size);
+		data_offset=size;
+
+		size += data_size;
+	}
 
 	/*
 	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add included columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/* The start of include attributes tuple is not aligned by default. All values
+		 * alignment should be done by heap_fill_tuple automaticaly. If there is a nulls
+		 * mask it is included just after key attribute data and it should not be aligned.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum+1, isnull+1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL) );
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +876,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed*/
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +923,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1234,128 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;/* offset of include data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+						 state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char*) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+					{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+					}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+		/* general case: can use cache until first null or varlen attribute */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true; 			/* can't use attcacheoff at all*/
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;		/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if the
+					 * offset is already suitably aligned, so that there would be no
+					 * pad bytes in any case: then the offset will be valid for either
+					 * an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;		/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..a9433f0ad4 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,25 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			// Dead tuple nextOffset is allowed to have highest bit 0 or 1 in case it is
+			// inherited from SpGistLeafTuple where it has its own meaning.
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +203,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +252,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +303,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +368,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index e976201030..514d5e21e4 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2,8 +2,6 @@
  *
  * elog.c
  *	  error logging and reporting
- *
- * Because of the extremely high rate at which log messages can be generated,
  * we need to be mindful of the performance cost of obtaining any information
  * that may be logged.  Also, it's important to keep in mind that this code may
  * get called from within an aborted transaction, in which case operations
@@ -244,6 +242,7 @@ errstart(int elevel, const char *domain)
 	 */
 	if (elevel >= ERROR)
 	{
+	//	abort();
 		/*
 		 * If we are inside a critical section, all errors become PANIC
 		 * errors.  See miscadmin.h.
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..c16ee8c322 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -141,6 +141,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc      includeTupdesc;	/* tuple descriptor of included columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,6 +149,91 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
+/*
+ * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ *
+ * In the simplest case, the datum is the same as the indexed value; but
+ * it could also be a suffix or some other sort of delta that permits
+ * reconstruction given knowledge of the prefix path traversed to get here.
+ *
+ * The size field is wider than could possibly be needed for an on-disk leaf
+ * tuple, but this allows us to form leaf tuples even when the datum is too
+ * wide to be stored immediately, and it costs nothing because of alignment
+ * considerations.
+ *
+ * Normally, nextOffset links to the next tuple belonging to the same parent
+ * node (which must be on the same page).  But when the root page is a leaf
+ * page, we don't chain its tuples, so nextOffset is always 0 on the root.
+ *
+ * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
+ * so that the tuple can be converted to REDIRECT status later.  (This
+ * restriction only adds bytes for the null-datum case, otherwise alignment
+ * restrictions force it anyway.)
+ *
+ * In a leaf tuple for a NULL indexed value, there's no useful datum value;
+ * however, the SGDTSIZE limit ensures that's there's a Datum word there
+ * anyway, so SGLTDATUM can be applied safely as long as you don't do
+ * anything with the result.
+ *
+ * As SpGistLeafTuple has header of 8 bytes so max value for nextOffset is
+ * (when page size is 65KB) is 8192 and 15 bit is sufficient to store it. So
+ * higher bit is reserved to store information is there nulls mask between leaf
+ * datum and first include value (if any). Size of null mask is 1 byte per each 8
+ * include columns.
+ */
+
+typedef struct SpGistLeafTupleData
+{
+	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
+				size:30;		/* large enough for any palloc'able value */
+	OffsetNumber nextOffset;	/* higher 1 bit = 1 if included values has nulls
+										  2 bit = 1 if included values contain variable length values
+								   lower 15 bits - nextOffset - points to the next tuple in chain,
+								   or InvalidOffsetNumber. They SHOULD NOT be set/read directly,
+								   SGLT_SET_OFFSET/SGLT_GET_OFFSET macro must be used instead. */
+	ItemPointerData heapPtr;	/* TID of represented heap tuple */
+	/* leaf datum follows */
+	/* if SGLT_GET_CONTAINSNULLMASK nullmask follows. Its size (number of included columns/8)+1 */
+	/* include attributes follow if any*/
+} SpGistLeafTupleData;
+
+typedef SpGistLeafTupleData *SpGistLeafTuple;
+
+#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
+#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
+#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Accessor macros for nextOffset and null mask presence bit.
+ * It's a bit of hack that these macros also safely apply to IncludeTupMetadata which has the same
+ * structure. Include tuple size of maximum 13 bits (see INDEX_SIZE_MASK) is stored there instead
+ * of NextOffset which is 14 bits. IncludeTupMetadata is a vehicle to transfer included tuple header
+ * as IncludeTuple is now filled before SpGistLeafTuple initialized.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
+
+#define SGLT_GET_INCLUDE_TUPSIZE(x) SGLT_GET_OFFSET(x)
+#define SGLT_SET_INCLUDE_TUPSIZE(x,o) SGLT_SET_OFFSET(x,o)
+
+extern char *SpGistFormIncludeTuple(TupleDesc tupleDescriptor, Datum *values,
+									bool *isnull, uint16 *tupdata);
+/*
+ * SPGiST dead tuple: declaration for examining non-live tuples
+ *
+ * The tupstate field of this struct must match those of regular inner and
+ * leaf tuples, and its size field must match a leaf tuple's.
+ * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
+ * field, to satisfy some Asserts that we make when replacing a leaf tuple
+ * with a dead tuple.
+ * We don't use nextOffset, but it's needed to align the pointer field.
+ */
+
 typedef struct SpGistSearchItem
 {
 	pairingheap_node phNode;	/* pairing heap node */
@@ -160,14 +246,14 @@ typedef struct SpGistSearchItem
 	bool		isLeaf;			/* SearchItem is heap item */
 	bool		recheck;		/* qual recheck is needed */
 	bool		recheckDistances;	/* distance recheck is needed */
-
+	SpGistLeafTuple leafTuple;
 	/* array with numberOfOrderBys entries */
 	double		distances[FLEXIBLE_ARRAY_MEMBER];
+	/* if there are include columns SpGistLeafTupleData follow */
 } SpGistSearchItem;
 
 #define SizeOfSpGistSearchItem(n_distances) \
 	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -241,6 +327,7 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc      includeTupdesc;
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
 } SpGistCache;
@@ -321,60 +408,6 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 *(Datum *) SGNTDATAPTR(x) : \
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
-/*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
- *
- * In the simplest case, the datum is the same as the indexed value; but
- * it could also be a suffix or some other sort of delta that permits
- * reconstruction given knowledge of the prefix path traversed to get here.
- *
- * The size field is wider than could possibly be needed for an on-disk leaf
- * tuple, but this allows us to form leaf tuples even when the datum is too
- * wide to be stored immediately, and it costs nothing because of alignment
- * considerations.
- *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
- * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
- * restriction only adds bytes for the null-datum case, otherwise alignment
- * restrictions force it anyway.)
- *
- * In a leaf tuple for a NULL indexed value, there's no useful datum value;
- * however, the SGDTSIZE limit ensures that's there's a Datum word there
- * anyway, so SGLTDATUM can be applied safely as long as you don't do
- * anything with the result.
- */
-typedef struct SpGistLeafTupleData
-{
-	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
-				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
-	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
-} SpGistLeafTupleData;
-
-typedef SpGistLeafTupleData *SpGistLeafTuple;
-
-#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
-#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
-
-/*
- * SPGiST dead tuple: declaration for examining non-live tuples
- *
- * The tupstate field of this struct must match those of regular inner and
- * leaf tuples, and its size field must match a leaf tuple's.
- * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
- * field, to satisfy some Asserts that we make when replacing a leaf tuple
- * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
- * pointer and xid are only valid when tupstate = REDIRECT.
- */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
@@ -394,7 +427,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -456,9 +488,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +499,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +519,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..4fd2b7e878 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -356,7 +356,6 @@ CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..fa64766fb7
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,139 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..a59e73aa22
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,81 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+DROP TABLE tbl_spgist;
+
#2Andrey M. Borodin
x4mmm@yandex-team.ru
In reply to: Pavel Borisov (#1)
Re: [PATCH] Covering SPGiST index

7 авг. 2020 г., в 16:59, Pavel Borisov <pashkin.elfe@gmail.com> написал(а):

As usual I very much appreciate your feedback

Thanks for the patch! Looks interesting.

On a first glance the whole concept of non-multicolumn index with included attributes seems...well, just difficult to understand.
But I expect for SP-GiST this must be single key with multiple included attributes, right?
I couldn't find a test that checks impossibility of on 2-column SP-GiST, only few asserts about it. Is this checked somewhere else?

Thanks!

Best regards, Andrey Borodin.

#3Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Andrey M. Borodin (#2)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

On a first glance the whole concept of non-multicolumn index with included
attributes seems...well, just difficult to understand.
But I expect for SP-GiST this must be single key with multiple included
attributes, right?
I couldn't find a test that checks impossibility of on 2-column SP-GiST,
only few asserts about it. Is this checked somewhere else?

Yes, SpGist is by its construction a single-column index, there is no such
thing like 2-column SP-GiST yet. In the same way like original SpGist will
refuse to add a second key column, this remains after modification as well,
with exception of columns attached by INCLUDE directive. They can be
(INDEX_MAX_KEYS -1) pieces and they will not be used to create additional
index trees (as there is only one), they will be just attached to the key
tree leafs tuple.

I also little bit corrected error reporting for the case when user wants to
invoke index build with not one column. Thanks!

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

spgist-covering-0002.diffapplication/octet-stream; name=spgist-covering-0002.diffDownload
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..b767a805fa 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET (leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset,InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset,r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum  		*leafChainDatums;
+	bool 	 	*leafChainIsnulls;
+	const int natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n*natts*sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n*natts*sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,14 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in a chain are collected
+	 * into 2-d arrays: (number of tuples in chain) x (number of attributes)
+	 * First attribute is key, the other - included attributes (if any). After
+	 * picksplit we need to form new leaf tuples as key attribute length can
+	 * change which can affect alignment of every include attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +771,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				SpGistDeformLeafTuple(it, state, leafChainDatums+nToInsert*natts,
+									  leafChainIsnulls+nToInsert*natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +798,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				SpGistDeformLeafTuple(it, state, leafChainDatums+nToInsert*natts,
+									  leafChainIsnulls+nToInsert*natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +812,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +820,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +833,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	SpGistDeformLeafTuple(newLeafTuple, state, leafChainDatums+(in.nTuples)*natts,
+						  leafChainIsnulls+(in.nTuples)*natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values are used
+	 * to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +861,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums+i*natts)=(Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls+i*natts)=false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums+i*natts,
+										   leafChainIsnulls+i*natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +886,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums+i*natts)=(Datum) 0;
+			*(leafChainIsnulls+i*natts)=true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums+i*natts,
+										   leafChainIsnulls+i*natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1227,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1920,81 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum		*leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
-	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/* If an optional "compress" method is provided, then call it to form the
+	 * key datum from the input datum.  Otherwise store the input datum as
 	 * is.  Since we don't use index_form_tuple in this AM, we have to make
 	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
 	 * guarantee that.  But we assume the "compress" method to return an
 	 * untoasted value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
 										  index->rd_indcollation[0],
-										  datum);
+										  datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+		{
+			if (!isnull[i])
+			{
+				if (TupleDescAttr(state->includeTupdesc, i-1)->attlen == -1)
+					leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+				else
+					leafDatum[i]=datum[i];
+			}
+			else
+				leafDatum[i]=(Datum) 0;
+		}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = SpgLeafSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2006,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2040,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2082,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2099,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2113,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2155,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2166,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2203,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = SpgLeafSize(state, leafDatum, isnull) +
+								   sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2272,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..b5dedc3afd 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,29 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
+	/*
+	 * If there are include attributes search item in the queue should
+	 * contain them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
 
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +524,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,13 +551,12 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
 														isnull,
 														distances);
-
 			spgAddSearchItemToQueue(so, heapItem);
 
 			MemoryContextSwitchTo(oldCxt);
@@ -543,8 +565,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+							 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +760,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +774,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +806,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +901,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +928,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -923,7 +947,7 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 
 			for (i = 0; i < so->numberOfOrderBys; i++)
 			{
-				int			offset = so->nonNullOrderByOffsets[i];
+				int		offset = so->nonNullOrderByOffsets[i];
 
 				if (offset >= 0)
 				{
@@ -949,9 +973,35 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+		if (so->state.includeTupdesc)
+		{
+			/* Add included attributes */
+			Datum *leafDatums;
+			bool *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			SpGistDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/* override key value extracted from LeafTuple in case we've reconstructed it already */
+			leafDatums[0]=leafValue;
+			leafIsnulls[0]=isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+												   leafDatums,
+												   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
 												   &leafValue,
 												   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1018,6 +1068,9 @@ bool
 spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
+	
+	/* Included attributes always can be fetched for index-only scans */
+	if (attno > 1) return true;
 
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..d052d52c25 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,8 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
-
-
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
+
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								  bool *isnull, Size start);
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
  * and callbacks.
@@ -49,7 +59,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +67,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -112,18 +122,21 @@ spgGetCache(Relation index)
 		FmgrInfo   *procinfo;
 		Buffer		metabuffer;
 		SpGistMetaPageData *metadata;
-
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/* SPGiST should have one key column and can also have included columns */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+			(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
+		 * Get the actual data type of the key column from the index
 		 * tupdesc.  We pass this to the opclass config function so that
 		 * polymorphic opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +169,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +191,22 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+		/* Form descriptor for included columns if any */
+		if (IndexRelationGetNumberOfAttributes(index) > 1)
+		{
+			int i;
+			cache->includeTupdesc = CreateTemplateTupleDesc(
+							IndexRelationGetNumberOfAttributes(index) - 1);
 
+			for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+			{
+				TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+								   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+								   -1, 0);
+			}
+		}
+		else
+			cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +219,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,7 +633,7 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
+ * Note the result is not maxaligned and this should be done by caller if needed.
  * Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
@@ -619,7 +649,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +672,198 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * necessarily MAXALIGNed. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of included
+ * data area in SpGiST leaf tuple.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					   Datum *values,
+					   bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length-start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed only for backward
+ * compatibility and there might be gap between header and key data. After key
+ * data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed.*/
+unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for include attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_COLUMNS),
+				 errmsg("number of index columns (%d) exceeds limit (%d)",
+						state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall included attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum+1, isnull+1, size);
+	}
+return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and included
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to included attributes and is placed just after key data if
+ * there is at least one NULL among included attributes. It doesn't need alignment.
+ * Then all included columns data follow aligned by their typealign's.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size=SGLTHDRSZ;
+	unsigned int include_offset=0;
+	unsigned int nullmask_size=0;
+	unsigned int data_offset=0;
+	unsigned int data_size=0;
+	uint16 tupmask=0;
+	int i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are include attributes also calculate sizes and
+	 * offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+				(errcode(ERRCODE_TOO_MANY_COLUMNS),
+				 errmsg("number of index columns (%d) exceeds limit (%d)",
+						state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all included attributes is counted inside data_size. data_offset
+		 * itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum+1, isnull+1, size);
+		data_offset=size;
+
+		size += data_size;
+	}
 
 	/*
 	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add included columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/* The start of include attributes tuple is not aligned by default. All values
+		 * alignment should be done by heap_fill_tuple automaticaly. If there is a nulls
+		 * mask it is included just after key attribute data and it should not be aligned.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum+1, isnull+1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL) );
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +880,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed*/
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +927,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1238,128 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;/* offset of include data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+						 state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char*) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+					{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+					}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+		/* general case: can use cache until first null or varlen attribute */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true; 			/* can't use attcacheoff at all*/
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;		/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if the
+					 * offset is already suitably aligned, so that there would be no
+					 * pad bytes in any case: then the offset will be valid for either
+					 * an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;		/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..a9433f0ad4 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,25 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			// Dead tuple nextOffset is allowed to have highest bit 0 or 1 in case it is
+			// inherited from SpGistLeafTuple where it has its own meaning.
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +203,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +252,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +303,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +368,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index e976201030..514d5e21e4 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -2,8 +2,6 @@
  *
  * elog.c
  *	  error logging and reporting
- *
- * Because of the extremely high rate at which log messages can be generated,
  * we need to be mindful of the performance cost of obtaining any information
  * that may be logged.  Also, it's important to keep in mind that this code may
  * get called from within an aborted transaction, in which case operations
@@ -244,6 +242,7 @@ errstart(int elevel, const char *domain)
 	 */
 	if (elevel >= ERROR)
 	{
+	//	abort();
 		/*
 		 * If we are inside a critical section, all errors become PANIC
 		 * errors.  See miscadmin.h.
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..c16ee8c322 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -141,6 +141,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc      includeTupdesc;	/* tuple descriptor of included columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,6 +149,91 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
+/*
+ * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ *
+ * In the simplest case, the datum is the same as the indexed value; but
+ * it could also be a suffix or some other sort of delta that permits
+ * reconstruction given knowledge of the prefix path traversed to get here.
+ *
+ * The size field is wider than could possibly be needed for an on-disk leaf
+ * tuple, but this allows us to form leaf tuples even when the datum is too
+ * wide to be stored immediately, and it costs nothing because of alignment
+ * considerations.
+ *
+ * Normally, nextOffset links to the next tuple belonging to the same parent
+ * node (which must be on the same page).  But when the root page is a leaf
+ * page, we don't chain its tuples, so nextOffset is always 0 on the root.
+ *
+ * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
+ * so that the tuple can be converted to REDIRECT status later.  (This
+ * restriction only adds bytes for the null-datum case, otherwise alignment
+ * restrictions force it anyway.)
+ *
+ * In a leaf tuple for a NULL indexed value, there's no useful datum value;
+ * however, the SGDTSIZE limit ensures that's there's a Datum word there
+ * anyway, so SGLTDATUM can be applied safely as long as you don't do
+ * anything with the result.
+ *
+ * As SpGistLeafTuple has header of 8 bytes so max value for nextOffset is
+ * (when page size is 65KB) is 8192 and 15 bit is sufficient to store it. So
+ * higher bit is reserved to store information is there nulls mask between leaf
+ * datum and first include value (if any). Size of null mask is 1 byte per each 8
+ * include columns.
+ */
+
+typedef struct SpGistLeafTupleData
+{
+	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
+				size:30;		/* large enough for any palloc'able value */
+	OffsetNumber nextOffset;	/* higher 1 bit = 1 if included values has nulls
+										  2 bit = 1 if included values contain variable length values
+								   lower 15 bits - nextOffset - points to the next tuple in chain,
+								   or InvalidOffsetNumber. They SHOULD NOT be set/read directly,
+								   SGLT_SET_OFFSET/SGLT_GET_OFFSET macro must be used instead. */
+	ItemPointerData heapPtr;	/* TID of represented heap tuple */
+	/* leaf datum follows */
+	/* if SGLT_GET_CONTAINSNULLMASK nullmask follows. Its size (number of included columns/8)+1 */
+	/* include attributes follow if any*/
+} SpGistLeafTupleData;
+
+typedef SpGistLeafTupleData *SpGistLeafTuple;
+
+#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
+#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
+#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Accessor macros for nextOffset and null mask presence bit.
+ * It's a bit of hack that these macros also safely apply to IncludeTupMetadata which has the same
+ * structure. Include tuple size of maximum 13 bits (see INDEX_SIZE_MASK) is stored there instead
+ * of NextOffset which is 14 bits. IncludeTupMetadata is a vehicle to transfer included tuple header
+ * as IncludeTuple is now filled before SpGistLeafTuple initialized.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
+
+#define SGLT_GET_INCLUDE_TUPSIZE(x) SGLT_GET_OFFSET(x)
+#define SGLT_SET_INCLUDE_TUPSIZE(x,o) SGLT_SET_OFFSET(x,o)
+
+extern char *SpGistFormIncludeTuple(TupleDesc tupleDescriptor, Datum *values,
+									bool *isnull, uint16 *tupdata);
+/*
+ * SPGiST dead tuple: declaration for examining non-live tuples
+ *
+ * The tupstate field of this struct must match those of regular inner and
+ * leaf tuples, and its size field must match a leaf tuple's.
+ * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
+ * field, to satisfy some Asserts that we make when replacing a leaf tuple
+ * with a dead tuple.
+ * We don't use nextOffset, but it's needed to align the pointer field.
+ */
+
 typedef struct SpGistSearchItem
 {
 	pairingheap_node phNode;	/* pairing heap node */
@@ -160,14 +246,14 @@ typedef struct SpGistSearchItem
 	bool		isLeaf;			/* SearchItem is heap item */
 	bool		recheck;		/* qual recheck is needed */
 	bool		recheckDistances;	/* distance recheck is needed */
-
+	SpGistLeafTuple leafTuple;
 	/* array with numberOfOrderBys entries */
 	double		distances[FLEXIBLE_ARRAY_MEMBER];
+	/* if there are include columns SpGistLeafTupleData follow */
 } SpGistSearchItem;
 
 #define SizeOfSpGistSearchItem(n_distances) \
 	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -241,6 +327,7 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc      includeTupdesc;
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
 } SpGistCache;
@@ -321,60 +408,6 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 *(Datum *) SGNTDATAPTR(x) : \
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
-/*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
- *
- * In the simplest case, the datum is the same as the indexed value; but
- * it could also be a suffix or some other sort of delta that permits
- * reconstruction given knowledge of the prefix path traversed to get here.
- *
- * The size field is wider than could possibly be needed for an on-disk leaf
- * tuple, but this allows us to form leaf tuples even when the datum is too
- * wide to be stored immediately, and it costs nothing because of alignment
- * considerations.
- *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
- * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
- * restriction only adds bytes for the null-datum case, otherwise alignment
- * restrictions force it anyway.)
- *
- * In a leaf tuple for a NULL indexed value, there's no useful datum value;
- * however, the SGDTSIZE limit ensures that's there's a Datum word there
- * anyway, so SGLTDATUM can be applied safely as long as you don't do
- * anything with the result.
- */
-typedef struct SpGistLeafTupleData
-{
-	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
-				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
-	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
-} SpGistLeafTupleData;
-
-typedef SpGistLeafTupleData *SpGistLeafTuple;
-
-#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
-#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
-
-/*
- * SPGiST dead tuple: declaration for examining non-live tuples
- *
- * The tupstate field of this struct must match those of regular inner and
- * leaf tuples, and its size field must match a leaf tuple's.
- * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
- * field, to satisfy some Asserts that we make when replacing a leaf tuple
- * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
- * pointer and xid are only valid when tupstate = REDIRECT.
- */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
@@ -394,7 +427,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -456,9 +488,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +499,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +519,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..4fd2b7e878 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -356,7 +356,6 @@ CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..fa64766fb7
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,139 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..a59e73aa22
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,81 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+DROP TABLE tbl_spgist;
+
#4Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#3)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

Also little bit corrected code formatting.

Show quoted text

Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

spgist-covering-0003.diffapplication/octet-stream; name=spgist-covering-0003.diffDownload
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..4c133b7106 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in a chain are
+	 * collected into 2-d arrays: (number of tuples in chain) x (number of
+	 * attributes) First attribute is key, the other - included attributes (if
+	 * any). After picksplit we need to form new leaf tuples as key attribute
+	 * length can change which can affect alignment of every include
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +772,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +799,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +813,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +821,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +834,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	SpGistDeformLeafTuple(newLeafTuple, state, leafChainDatums + (in.nTuples) * natts,
+						  leafChainIsnulls + (in.nTuples) * natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +862,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums + i * natts) = (Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls + i * natts) = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +887,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums + i * natts) = (Datum) 0;
+			*(leafChainIsnulls + i * natts) = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1228,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1921,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum.  Otherwise store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = SpgLeafSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2009,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2043,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2085,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2116,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2158,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2169,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2206,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = SpgLeafSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..5a3c7c50cf 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are include attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add included attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			SpGistDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[0] = leafValue;
+			leafIsnulls[0] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* Included attributes always can be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..3ca47ff53d 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -116,14 +127,21 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have included
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +174,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +196,23 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+	/* Form descriptor for included columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
 
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +225,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,7 +639,7 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
+ * Note the result is not maxaligned and this should be done by caller if needed.
  * Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
@@ -619,7 +655,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +678,202 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * necessarily MAXALIGNed. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of included
+ * data area in SpGiST leaf tuple.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed only for backward
+ * compatibility and there might be gap between header and key data. After key
+ * data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed.*/
+unsigned int
+SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for include attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall included attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and included
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to included attributes and is placed just after key data if
+ * there is at least one NULL among included attributes. It doesn't need alignment.
+ * Then all included columns data follow aligned by their typealign's.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are include attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all included attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
 	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add included columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of include attributes tuple is not aligned by default.
+		 * All values alignment should be done by heap_fill_tuple
+		 * automaticaly. If there is a nulls mask it is included just after
+		 * key attribute data and it should not be aligned.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +890,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +937,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1248,133 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of include data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..a0d76901fc 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits has their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..8d03adb8f5 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -141,6 +141,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of included columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,6 +149,98 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
+/*
+ * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ *
+ * In the simplest case, the datum is the same as the indexed value; but
+ * it could also be a suffix or some other sort of delta that permits
+ * reconstruction given knowledge of the prefix path traversed to get here.
+ *
+ * The size field is wider than could possibly be needed for an on-disk leaf
+ * tuple, but this allows us to form leaf tuples even when the datum is too
+ * wide to be stored immediately, and it costs nothing because of alignment
+ * considerations.
+ *
+ * Normally, nextOffset links to the next tuple belonging to the same parent
+ * node (which must be on the same page).  But when the root page is a leaf
+ * page, we don't chain its tuples, so nextOffset is always 0 on the root.
+ *
+ * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
+ * so that the tuple can be converted to REDIRECT status later.  (This
+ * restriction only adds bytes for the null-datum case, otherwise alignment
+ * restrictions force it anyway.)
+ *
+ * In a leaf tuple for a NULL indexed value, there's no useful datum value;
+ * however, the SGDTSIZE limit ensures that's there's a Datum word there
+ * anyway, so SGLTDATUM can be applied safely as long as you don't do
+ * anything with the result.
+ *
+ * Minimum space to store SpGistLeafTuple on a page is 12 bytes tuple header
+ * and 4 bytes ItemIdData so 14 lower bits of nextOffset (accessed as
+ * SGLT_GET/SET_OFFSET) is enough to store actual tuple number on a page even
+ * if page size is 64Kb. Two higher bits are to store per-tuple
+ * information is there nulls mask exist and is there any included attribute
+ * of variable length type.
+ */
+
+typedef struct SpGistLeafTupleData
+{
+	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
+				size:30;		/* large enough for any palloc'able value */
+	OffsetNumber nextOffset;	/* higher 1 bit = 1 if included values has
+								 * nulls, 2 bit = 1 if included values contain
+								 * variable length values, lower 15 bits - is
+								 * "actual" nextOffset i.e. number of next
+								 * tuple in chain on a page, or
+								 * InvalidOffsetNumber. They SHOULD NOT be
+								 * set/read directly,
+								 * SGLT_SET_XXX/SGLT_GET_XXX macros must be
+								 * used instead. */
+	ItemPointerData heapPtr;	/* TID of represented heap tuple */
+	/* leaf datum follows */
+
+	/*
+	 * if SGLT_GET_CONTAINSNULLMASK nullmask follows. Its size (number of
+	 * included columns/8)+1
+	 */
+	/* include attributes follow if any */
+} SpGistLeafTupleData;
+
+typedef SpGistLeafTupleData *SpGistLeafTuple;
+
+#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
+#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
+#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Accessor macros to get and set actual 14-bit offset and two bit flags from/to
+ * nextOffset value.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
+
+#define SGLT_GET_INCLUDE_TUPSIZE(x) SGLT_GET_OFFSET(x)
+#define SGLT_SET_INCLUDE_TUPSIZE(x,o) SGLT_SET_OFFSET(x,o)
+
+extern char *SpGistFormIncludeTuple(TupleDesc tupleDescriptor, Datum *values,
+									bool *isnull, uint16 *tupdata);
+
+/*
+ * SPGiST dead tuple: declaration for examining non-live tuples
+ *
+ * The tupstate field of this struct must match those of regular inner and
+ * leaf tuples, and its size field must match a leaf tuple's.
+ * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
+ * field, to satisfy some Asserts that we make when replacing a leaf tuple
+ * with a dead tuple.
+ * We don't use nextOffset, but it's needed to align the pointer field.
+ */
+
 typedef struct SpGistSearchItem
 {
 	pairingheap_node phNode;	/* pairing heap node */
@@ -160,14 +253,14 @@ typedef struct SpGistSearchItem
 	bool		isLeaf;			/* SearchItem is heap item */
 	bool		recheck;		/* qual recheck is needed */
 	bool		recheckDistances;	/* distance recheck is needed */
-
+	SpGistLeafTuple leafTuple;
 	/* array with numberOfOrderBys entries */
 	double		distances[FLEXIBLE_ARRAY_MEMBER];
+	/* if there are include columns SpGistLeafTupleData follow */
 } SpGistSearchItem;
 
 #define SizeOfSpGistSearchItem(n_distances) \
 	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -241,6 +334,7 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
 } SpGistCache;
@@ -321,60 +415,6 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 *(Datum *) SGNTDATAPTR(x) : \
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
-/*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
- *
- * In the simplest case, the datum is the same as the indexed value; but
- * it could also be a suffix or some other sort of delta that permits
- * reconstruction given knowledge of the prefix path traversed to get here.
- *
- * The size field is wider than could possibly be needed for an on-disk leaf
- * tuple, but this allows us to form leaf tuples even when the datum is too
- * wide to be stored immediately, and it costs nothing because of alignment
- * considerations.
- *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
- * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
- * restriction only adds bytes for the null-datum case, otherwise alignment
- * restrictions force it anyway.)
- *
- * In a leaf tuple for a NULL indexed value, there's no useful datum value;
- * however, the SGDTSIZE limit ensures that's there's a Datum word there
- * anyway, so SGLTDATUM can be applied safely as long as you don't do
- * anything with the result.
- */
-typedef struct SpGistLeafTupleData
-{
-	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
-				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
-	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
-} SpGistLeafTupleData;
-
-typedef SpGistLeafTupleData *SpGistLeafTuple;
-
-#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
-#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
-
-/*
- * SPGiST dead tuple: declaration for examining non-live tuples
- *
- * The tupstate field of this struct must match those of regular inner and
- * leaf tuples, and its size field must match a leaf tuple's.
- * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
- * field, to satisfy some Asserts that we make when replacing a leaf tuple
- * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
- * pointer and xid are only valid when tupstate = REDIRECT.
- */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
@@ -394,7 +434,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -456,9 +495,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +506,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +526,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..4fd2b7e878 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -356,7 +356,6 @@ CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..fa64766fb7
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,139 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..a59e73aa22
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,81 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+DROP TABLE tbl_spgist;
+
#5Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#4)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

Same code formatted as a patch.

пн, 10 авг. 2020 г. в 17:45, Pavel Borisov <pashkin.elfe@gmail.com>:

Show quoted text

Also little bit corrected code formatting.

Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com
<http://www.postgrespro.com&gt;

Attachments:

v3-0001-Covering-SpGist.patchapplication/octet-stream; name=v3-0001-Covering-SpGist.patchDownload
From 6a44de4f93259d2325242b0080591b2799cf7a8c Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 10 Aug 2020 20:09:48 +0400
Subject: [PATCH v3] Covering SpGist

---
 src/backend/access/spgist/spgdoinsert.c       | 172 +++++---
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  87 +++-
 src/backend/access/spgist/spgutils.c          | 382 ++++++++++++++++--
 src/backend/access/spgist/spgvacuum.c         |  25 +-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/spgist_private.h           | 160 +++++---
 src/test/regress/expected/amutils.out         |   4 +-
 src/test/regress/expected/index_including.out |   1 -
 .../expected/index_including_spgist.out       | 139 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 .../regress/sql/index_including_spgist.sql    |  81 ++++
 13 files changed, 885 insertions(+), 180 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..4c133b7106 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in a chain are
+	 * collected into 2-d arrays: (number of tuples in chain) x (number of
+	 * attributes) First attribute is key, the other - included attributes (if
+	 * any). After picksplit we need to form new leaf tuples as key attribute
+	 * length can change which can affect alignment of every include
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +772,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +799,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +813,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +821,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +834,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	SpGistDeformLeafTuple(newLeafTuple, state, leafChainDatums + (in.nTuples) * natts,
+						  leafChainIsnulls + (in.nTuples) * natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +862,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums + i * natts) = (Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls + i * natts) = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +887,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums + i * natts) = (Datum) 0;
+			*(leafChainIsnulls + i * natts) = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1228,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1921,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum.  Otherwise store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = SpgLeafSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2009,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2043,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2085,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2116,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2158,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2169,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2206,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = SpgLeafSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..5a3c7c50cf 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are include attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add included attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			SpGistDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[0] = leafValue;
+			leafIsnulls[0] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* Included attributes always can be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..3ca47ff53d 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -116,14 +127,21 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have included
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +174,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +196,23 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+	/* Form descriptor for included columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
 
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +225,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,7 +639,7 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
+ * Note the result is not maxaligned and this should be done by caller if needed.
  * Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
@@ -619,7 +655,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +678,202 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * necessarily MAXALIGNed. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of included
+ * data area in SpGiST leaf tuple.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed only for backward
+ * compatibility and there might be gap between header and key data. After key
+ * data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed.*/
+unsigned int
+SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for include attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall included attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and included
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to included attributes and is placed just after key data if
+ * there is at least one NULL among included attributes. It doesn't need alignment.
+ * Then all included columns data follow aligned by their typealign's.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are include attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all included attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
 	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add included columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of include attributes tuple is not aligned by default.
+		 * All values alignment should be done by heap_fill_tuple
+		 * automaticaly. If there is a nulls mask it is included just after
+		 * key attribute data and it should not be aligned.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +890,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +937,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1248,133 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of include data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..a0d76901fc 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits has their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..8d03adb8f5 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -141,6 +141,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of included columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,6 +149,98 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
+/*
+ * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ *
+ * In the simplest case, the datum is the same as the indexed value; but
+ * it could also be a suffix or some other sort of delta that permits
+ * reconstruction given knowledge of the prefix path traversed to get here.
+ *
+ * The size field is wider than could possibly be needed for an on-disk leaf
+ * tuple, but this allows us to form leaf tuples even when the datum is too
+ * wide to be stored immediately, and it costs nothing because of alignment
+ * considerations.
+ *
+ * Normally, nextOffset links to the next tuple belonging to the same parent
+ * node (which must be on the same page).  But when the root page is a leaf
+ * page, we don't chain its tuples, so nextOffset is always 0 on the root.
+ *
+ * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
+ * so that the tuple can be converted to REDIRECT status later.  (This
+ * restriction only adds bytes for the null-datum case, otherwise alignment
+ * restrictions force it anyway.)
+ *
+ * In a leaf tuple for a NULL indexed value, there's no useful datum value;
+ * however, the SGDTSIZE limit ensures that's there's a Datum word there
+ * anyway, so SGLTDATUM can be applied safely as long as you don't do
+ * anything with the result.
+ *
+ * Minimum space to store SpGistLeafTuple on a page is 12 bytes tuple header
+ * and 4 bytes ItemIdData so 14 lower bits of nextOffset (accessed as
+ * SGLT_GET/SET_OFFSET) is enough to store actual tuple number on a page even
+ * if page size is 64Kb. Two higher bits are to store per-tuple
+ * information is there nulls mask exist and is there any included attribute
+ * of variable length type.
+ */
+
+typedef struct SpGistLeafTupleData
+{
+	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
+				size:30;		/* large enough for any palloc'able value */
+	OffsetNumber nextOffset;	/* higher 1 bit = 1 if included values has
+								 * nulls, 2 bit = 1 if included values contain
+								 * variable length values, lower 15 bits - is
+								 * "actual" nextOffset i.e. number of next
+								 * tuple in chain on a page, or
+								 * InvalidOffsetNumber. They SHOULD NOT be
+								 * set/read directly,
+								 * SGLT_SET_XXX/SGLT_GET_XXX macros must be
+								 * used instead. */
+	ItemPointerData heapPtr;	/* TID of represented heap tuple */
+	/* leaf datum follows */
+
+	/*
+	 * if SGLT_GET_CONTAINSNULLMASK nullmask follows. Its size (number of
+	 * included columns/8)+1
+	 */
+	/* include attributes follow if any */
+} SpGistLeafTupleData;
+
+typedef SpGistLeafTupleData *SpGistLeafTuple;
+
+#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
+#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
+#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Accessor macros to get and set actual 14-bit offset and two bit flags from/to
+ * nextOffset value.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
+
+#define SGLT_GET_INCLUDE_TUPSIZE(x) SGLT_GET_OFFSET(x)
+#define SGLT_SET_INCLUDE_TUPSIZE(x,o) SGLT_SET_OFFSET(x,o)
+
+extern char *SpGistFormIncludeTuple(TupleDesc tupleDescriptor, Datum *values,
+									bool *isnull, uint16 *tupdata);
+
+/*
+ * SPGiST dead tuple: declaration for examining non-live tuples
+ *
+ * The tupstate field of this struct must match those of regular inner and
+ * leaf tuples, and its size field must match a leaf tuple's.
+ * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
+ * field, to satisfy some Asserts that we make when replacing a leaf tuple
+ * with a dead tuple.
+ * We don't use nextOffset, but it's needed to align the pointer field.
+ */
+
 typedef struct SpGistSearchItem
 {
 	pairingheap_node phNode;	/* pairing heap node */
@@ -160,14 +253,14 @@ typedef struct SpGistSearchItem
 	bool		isLeaf;			/* SearchItem is heap item */
 	bool		recheck;		/* qual recheck is needed */
 	bool		recheckDistances;	/* distance recheck is needed */
-
+	SpGistLeafTuple leafTuple;
 	/* array with numberOfOrderBys entries */
 	double		distances[FLEXIBLE_ARRAY_MEMBER];
+	/* if there are include columns SpGistLeafTupleData follow */
 } SpGistSearchItem;
 
 #define SizeOfSpGistSearchItem(n_distances) \
 	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -241,6 +334,7 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
 } SpGistCache;
@@ -321,60 +415,6 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 *(Datum *) SGNTDATAPTR(x) : \
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
-/*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
- *
- * In the simplest case, the datum is the same as the indexed value; but
- * it could also be a suffix or some other sort of delta that permits
- * reconstruction given knowledge of the prefix path traversed to get here.
- *
- * The size field is wider than could possibly be needed for an on-disk leaf
- * tuple, but this allows us to form leaf tuples even when the datum is too
- * wide to be stored immediately, and it costs nothing because of alignment
- * considerations.
- *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
- * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
- * restriction only adds bytes for the null-datum case, otherwise alignment
- * restrictions force it anyway.)
- *
- * In a leaf tuple for a NULL indexed value, there's no useful datum value;
- * however, the SGDTSIZE limit ensures that's there's a Datum word there
- * anyway, so SGLTDATUM can be applied safely as long as you don't do
- * anything with the result.
- */
-typedef struct SpGistLeafTupleData
-{
-	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
-				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
-	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
-} SpGistLeafTupleData;
-
-typedef SpGistLeafTupleData *SpGistLeafTuple;
-
-#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
-#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
-
-/*
- * SPGiST dead tuple: declaration for examining non-live tuples
- *
- * The tupstate field of this struct must match those of regular inner and
- * leaf tuples, and its size field must match a leaf tuple's.
- * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
- * field, to satisfy some Asserts that we make when replacing a leaf tuple
- * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
- * pointer and xid are only valid when tupstate = REDIRECT.
- */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
@@ -394,7 +434,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -456,9 +495,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +506,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +526,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..4fd2b7e878 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -356,7 +356,6 @@ CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..fa64766fb7
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,139 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..a59e73aa22
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,81 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+DROP TABLE tbl_spgist;
+
-- 
2.28.0

#6Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#5)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

I added changes in documentation into the patch.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v4-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchapplication/octet-stream; name=v4-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchDownload
From f7d2a623b2c36ef69eb3c5b74fe2619e5f054e49 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Tue, 11 Aug 2020 12:05:51 +0400
Subject: [PATCH v4] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE colums for SPGiST index is intended to increase the speed of queries by making scan index only likewise
in btree and GiST index. These included values are added only to leaf tuples and they are not used in index tree search
but they can be fetched during index scan.

The other point of included columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in
certain cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster update etc. Also there can be included any data types without SP-GiST supported opclasses.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/spgist/README              |   2 +-
 src/backend/access/spgist/spgdoinsert.c       | 172 +++++---
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  87 +++-
 src/backend/access/spgist/spgutils.c          | 382 ++++++++++++++++--
 src/backend/access/spgist/spgvacuum.c         |  25 +-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/spgist_private.h           | 160 +++++---
 src/test/regress/expected/amutils.out         |   4 +-
 src/test/regress/expected/index_including.out |   1 -
 .../expected/index_including_spgist.out       | 139 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 .../regress/sql/index_including_spgist.sql    |  81 ++++
 17 files changed, 898 insertions(+), 185 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 28adaba72d..c89cc6cb08 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index ff87b2d28f..3d360bcf47 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 0e04a08679..868a140a6a 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -240,6 +240,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..87e08431fa 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -73,8 +73,8 @@ Leaf tuple consists of:
     Example:
         radix tree - the rest of string (postfix)
         quad and k-d tree - the point itself
-
   ItemPointer to the heap
+  optional included colums values
 
 
 NULLS HANDLING
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..4c133b7106 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in a chain are
+	 * collected into 2-d arrays: (number of tuples in chain) x (number of
+	 * attributes) First attribute is key, the other - included attributes (if
+	 * any). After picksplit we need to form new leaf tuples as key attribute
+	 * length can change which can affect alignment of every include
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +772,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +799,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +813,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +821,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +834,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	SpGistDeformLeafTuple(newLeafTuple, state, leafChainDatums + (in.nTuples) * natts,
+						  leafChainIsnulls + (in.nTuples) * natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +862,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums + i * natts) = (Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls + i * natts) = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +887,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums + i * natts) = (Datum) 0;
+			*(leafChainIsnulls + i * natts) = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1228,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1921,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum.  Otherwise store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = SpgLeafSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2009,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2043,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2085,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2116,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2158,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2169,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2206,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = SpgLeafSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..5a3c7c50cf 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are include attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add included attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			SpGistDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[0] = leafValue;
+			leafIsnulls[0] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* Included attributes always can be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..3ca47ff53d 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -116,14 +127,21 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have included
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +174,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +196,23 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+	/* Form descriptor for included columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
 
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +225,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,7 +639,7 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
+ * Note the result is not maxaligned and this should be done by caller if needed.
  * Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
@@ -619,7 +655,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +678,202 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * necessarily MAXALIGNed. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of included
+ * data area in SpGiST leaf tuple.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed only for backward
+ * compatibility and there might be gap between header and key data. After key
+ * data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed.*/
+unsigned int
+SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for include attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall included attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and included
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to included attributes and is placed just after key data if
+ * there is at least one NULL among included attributes. It doesn't need alignment.
+ * Then all included columns data follow aligned by their typealign's.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are include attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all included attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
 	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add included columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of include attributes tuple is not aligned by default.
+		 * All values alignment should be done by heap_fill_tuple
+		 * automaticaly. If there is a nulls mask it is included just after
+		 * key attribute data and it should not be aligned.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +890,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +937,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1248,133 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of include data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..a0d76901fc 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits has their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..8d03adb8f5 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -141,6 +141,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of included columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,6 +149,98 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
+/*
+ * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ *
+ * In the simplest case, the datum is the same as the indexed value; but
+ * it could also be a suffix or some other sort of delta that permits
+ * reconstruction given knowledge of the prefix path traversed to get here.
+ *
+ * The size field is wider than could possibly be needed for an on-disk leaf
+ * tuple, but this allows us to form leaf tuples even when the datum is too
+ * wide to be stored immediately, and it costs nothing because of alignment
+ * considerations.
+ *
+ * Normally, nextOffset links to the next tuple belonging to the same parent
+ * node (which must be on the same page).  But when the root page is a leaf
+ * page, we don't chain its tuples, so nextOffset is always 0 on the root.
+ *
+ * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
+ * so that the tuple can be converted to REDIRECT status later.  (This
+ * restriction only adds bytes for the null-datum case, otherwise alignment
+ * restrictions force it anyway.)
+ *
+ * In a leaf tuple for a NULL indexed value, there's no useful datum value;
+ * however, the SGDTSIZE limit ensures that's there's a Datum word there
+ * anyway, so SGLTDATUM can be applied safely as long as you don't do
+ * anything with the result.
+ *
+ * Minimum space to store SpGistLeafTuple on a page is 12 bytes tuple header
+ * and 4 bytes ItemIdData so 14 lower bits of nextOffset (accessed as
+ * SGLT_GET/SET_OFFSET) is enough to store actual tuple number on a page even
+ * if page size is 64Kb. Two higher bits are to store per-tuple
+ * information is there nulls mask exist and is there any included attribute
+ * of variable length type.
+ */
+
+typedef struct SpGistLeafTupleData
+{
+	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
+				size:30;		/* large enough for any palloc'able value */
+	OffsetNumber nextOffset;	/* higher 1 bit = 1 if included values has
+								 * nulls, 2 bit = 1 if included values contain
+								 * variable length values, lower 15 bits - is
+								 * "actual" nextOffset i.e. number of next
+								 * tuple in chain on a page, or
+								 * InvalidOffsetNumber. They SHOULD NOT be
+								 * set/read directly,
+								 * SGLT_SET_XXX/SGLT_GET_XXX macros must be
+								 * used instead. */
+	ItemPointerData heapPtr;	/* TID of represented heap tuple */
+	/* leaf datum follows */
+
+	/*
+	 * if SGLT_GET_CONTAINSNULLMASK nullmask follows. Its size (number of
+	 * included columns/8)+1
+	 */
+	/* include attributes follow if any */
+} SpGistLeafTupleData;
+
+typedef SpGistLeafTupleData *SpGistLeafTuple;
+
+#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
+#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
+#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Accessor macros to get and set actual 14-bit offset and two bit flags from/to
+ * nextOffset value.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
+
+#define SGLT_GET_INCLUDE_TUPSIZE(x) SGLT_GET_OFFSET(x)
+#define SGLT_SET_INCLUDE_TUPSIZE(x,o) SGLT_SET_OFFSET(x,o)
+
+extern char *SpGistFormIncludeTuple(TupleDesc tupleDescriptor, Datum *values,
+									bool *isnull, uint16 *tupdata);
+
+/*
+ * SPGiST dead tuple: declaration for examining non-live tuples
+ *
+ * The tupstate field of this struct must match those of regular inner and
+ * leaf tuples, and its size field must match a leaf tuple's.
+ * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
+ * field, to satisfy some Asserts that we make when replacing a leaf tuple
+ * with a dead tuple.
+ * We don't use nextOffset, but it's needed to align the pointer field.
+ */
+
 typedef struct SpGistSearchItem
 {
 	pairingheap_node phNode;	/* pairing heap node */
@@ -160,14 +253,14 @@ typedef struct SpGistSearchItem
 	bool		isLeaf;			/* SearchItem is heap item */
 	bool		recheck;		/* qual recheck is needed */
 	bool		recheckDistances;	/* distance recheck is needed */
-
+	SpGistLeafTuple leafTuple;
 	/* array with numberOfOrderBys entries */
 	double		distances[FLEXIBLE_ARRAY_MEMBER];
+	/* if there are include columns SpGistLeafTupleData follow */
 } SpGistSearchItem;
 
 #define SizeOfSpGistSearchItem(n_distances) \
 	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -241,6 +334,7 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
 } SpGistCache;
@@ -321,60 +415,6 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 *(Datum *) SGNTDATAPTR(x) : \
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
-/*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
- *
- * In the simplest case, the datum is the same as the indexed value; but
- * it could also be a suffix or some other sort of delta that permits
- * reconstruction given knowledge of the prefix path traversed to get here.
- *
- * The size field is wider than could possibly be needed for an on-disk leaf
- * tuple, but this allows us to form leaf tuples even when the datum is too
- * wide to be stored immediately, and it costs nothing because of alignment
- * considerations.
- *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
- * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
- * restriction only adds bytes for the null-datum case, otherwise alignment
- * restrictions force it anyway.)
- *
- * In a leaf tuple for a NULL indexed value, there's no useful datum value;
- * however, the SGDTSIZE limit ensures that's there's a Datum word there
- * anyway, so SGLTDATUM can be applied safely as long as you don't do
- * anything with the result.
- */
-typedef struct SpGistLeafTupleData
-{
-	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
-				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
-	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
-} SpGistLeafTupleData;
-
-typedef SpGistLeafTupleData *SpGistLeafTuple;
-
-#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
-#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
-
-/*
- * SPGiST dead tuple: declaration for examining non-live tuples
- *
- * The tupstate field of this struct must match those of regular inner and
- * leaf tuples, and its size field must match a leaf tuple's.
- * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
- * field, to satisfy some Asserts that we make when replacing a leaf tuple
- * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
- * pointer and xid are only valid when tupstate = REDIRECT.
- */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
@@ -394,7 +434,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -456,9 +495,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +506,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +526,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..4fd2b7e878 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -356,7 +356,6 @@ CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..fa64766fb7
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,139 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..a59e73aa22
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,81 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+DROP TABLE tbl_spgist;
+
-- 
2.28.0

#7Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#6)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

вт, 11 авг. 2020 г. в 12:11, Pavel Borisov <pashkin.elfe@gmail.com>:

Show quoted text

I added changes in documentation into the patch.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v5-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchapplication/octet-stream; name=v5-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchDownload
From 52d67cfbfe09ba085e4316f243e2df1a12f6f3c4 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Tue, 11 Aug 2020 22:49:22 +0400
Subject: [PATCH v5] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE colums for SPGiST index is intended to increase the speed of queries by making scan index only likewise
in btree and GiST index. These included values are added only to leaf tuples and they are not used in index tree search
but they can be fetched during index scan.

The other point of included columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in
certain cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster update etc. Also there can be included any data types without SP-GiST supported opclasses.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/spgist/README              |   2 +-
 src/backend/access/spgist/spgdoinsert.c       | 172 +++++---
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  87 +++-
 src/backend/access/spgist/spgutils.c          | 384 ++++++++++++++++--
 src/backend/access/spgist/spgvacuum.c         |  25 +-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/spgist_private.h           | 160 +++++---
 src/test/regress/expected/amutils.out         |   4 +-
 src/test/regress/expected/index_including.out |   1 -
 .../expected/index_including_spgist.out       | 139 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 .../regress/sql/index_including_spgist.sql    |  81 ++++
 17 files changed, 900 insertions(+), 185 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 28adaba72d..c89cc6cb08 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index ff87b2d28f..3d360bcf47 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 0e04a08679..868a140a6a 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -240,6 +240,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..87e08431fa 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -73,8 +73,8 @@ Leaf tuple consists of:
     Example:
         radix tree - the rest of string (postfix)
         quad and k-d tree - the point itself
-
   ItemPointer to the heap
+  optional included colums values
 
 
 NULLS HANDLING
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..4c133b7106 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in a chain are
+	 * collected into 2-d arrays: (number of tuples in chain) x (number of
+	 * attributes) First attribute is key, the other - included attributes (if
+	 * any). After picksplit we need to form new leaf tuples as key attribute
+	 * length can change which can affect alignment of every include
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +772,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +799,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +813,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +821,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +834,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	SpGistDeformLeafTuple(newLeafTuple, state, leafChainDatums + (in.nTuples) * natts,
+						  leafChainIsnulls + (in.nTuples) * natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +862,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums + i * natts) = (Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls + i * natts) = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +887,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums + i * natts) = (Datum) 0;
+			*(leafChainIsnulls + i * natts) = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1228,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1921,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum.  Otherwise store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = SpgLeafSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2009,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2043,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2085,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2116,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2158,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2169,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2206,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = SpgLeafSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..5a3c7c50cf 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are include attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add included attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			SpGistDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[0] = leafValue;
+			leafIsnulls[0] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* Included attributes always can be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..93c99fca4b 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -116,14 +127,21 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have included
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +174,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +196,25 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+	/* Form descriptor for included columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
+
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+			TupleDescAttr(cache->includeTupdesc, i)->attstorage = TYPSTORAGE_PLAIN;
 
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +227,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,7 +641,7 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
+ * Note the result is not maxaligned and this should be done by caller if needed.
  * Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
@@ -619,7 +657,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +680,202 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * necessarily MAXALIGNed. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of included
+ * data area in SpGiST leaf tuple.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed only for backward
+ * compatibility and there might be gap between header and key data. After key
+ * data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed.*/
+unsigned int
+SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for include attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall included attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and included
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to included attributes and is placed just after key data if
+ * there is at least one NULL among included attributes. It doesn't need alignment.
+ * Then all included columns data follow aligned by their typealign's.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are include attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all included attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
 	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add included columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of include attributes tuple is not aligned by default.
+		 * All values alignment should be done by heap_fill_tuple
+		 * automaticaly. If there is a nulls mask it is included just after
+		 * key attribute data and it should not be aligned.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +892,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +939,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1250,133 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of include data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..a0d76901fc 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits has their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..8d03adb8f5 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -141,6 +141,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of included columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,6 +149,98 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
+/*
+ * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ *
+ * In the simplest case, the datum is the same as the indexed value; but
+ * it could also be a suffix or some other sort of delta that permits
+ * reconstruction given knowledge of the prefix path traversed to get here.
+ *
+ * The size field is wider than could possibly be needed for an on-disk leaf
+ * tuple, but this allows us to form leaf tuples even when the datum is too
+ * wide to be stored immediately, and it costs nothing because of alignment
+ * considerations.
+ *
+ * Normally, nextOffset links to the next tuple belonging to the same parent
+ * node (which must be on the same page).  But when the root page is a leaf
+ * page, we don't chain its tuples, so nextOffset is always 0 on the root.
+ *
+ * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
+ * so that the tuple can be converted to REDIRECT status later.  (This
+ * restriction only adds bytes for the null-datum case, otherwise alignment
+ * restrictions force it anyway.)
+ *
+ * In a leaf tuple for a NULL indexed value, there's no useful datum value;
+ * however, the SGDTSIZE limit ensures that's there's a Datum word there
+ * anyway, so SGLTDATUM can be applied safely as long as you don't do
+ * anything with the result.
+ *
+ * Minimum space to store SpGistLeafTuple on a page is 12 bytes tuple header
+ * and 4 bytes ItemIdData so 14 lower bits of nextOffset (accessed as
+ * SGLT_GET/SET_OFFSET) is enough to store actual tuple number on a page even
+ * if page size is 64Kb. Two higher bits are to store per-tuple
+ * information is there nulls mask exist and is there any included attribute
+ * of variable length type.
+ */
+
+typedef struct SpGistLeafTupleData
+{
+	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
+				size:30;		/* large enough for any palloc'able value */
+	OffsetNumber nextOffset;	/* higher 1 bit = 1 if included values has
+								 * nulls, 2 bit = 1 if included values contain
+								 * variable length values, lower 15 bits - is
+								 * "actual" nextOffset i.e. number of next
+								 * tuple in chain on a page, or
+								 * InvalidOffsetNumber. They SHOULD NOT be
+								 * set/read directly,
+								 * SGLT_SET_XXX/SGLT_GET_XXX macros must be
+								 * used instead. */
+	ItemPointerData heapPtr;	/* TID of represented heap tuple */
+	/* leaf datum follows */
+
+	/*
+	 * if SGLT_GET_CONTAINSNULLMASK nullmask follows. Its size (number of
+	 * included columns/8)+1
+	 */
+	/* include attributes follow if any */
+} SpGistLeafTupleData;
+
+typedef SpGistLeafTupleData *SpGistLeafTuple;
+
+#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
+#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
+#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Accessor macros to get and set actual 14-bit offset and two bit flags from/to
+ * nextOffset value.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
+
+#define SGLT_GET_INCLUDE_TUPSIZE(x) SGLT_GET_OFFSET(x)
+#define SGLT_SET_INCLUDE_TUPSIZE(x,o) SGLT_SET_OFFSET(x,o)
+
+extern char *SpGistFormIncludeTuple(TupleDesc tupleDescriptor, Datum *values,
+									bool *isnull, uint16 *tupdata);
+
+/*
+ * SPGiST dead tuple: declaration for examining non-live tuples
+ *
+ * The tupstate field of this struct must match those of regular inner and
+ * leaf tuples, and its size field must match a leaf tuple's.
+ * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
+ * field, to satisfy some Asserts that we make when replacing a leaf tuple
+ * with a dead tuple.
+ * We don't use nextOffset, but it's needed to align the pointer field.
+ */
+
 typedef struct SpGistSearchItem
 {
 	pairingheap_node phNode;	/* pairing heap node */
@@ -160,14 +253,14 @@ typedef struct SpGistSearchItem
 	bool		isLeaf;			/* SearchItem is heap item */
 	bool		recheck;		/* qual recheck is needed */
 	bool		recheckDistances;	/* distance recheck is needed */
-
+	SpGistLeafTuple leafTuple;
 	/* array with numberOfOrderBys entries */
 	double		distances[FLEXIBLE_ARRAY_MEMBER];
+	/* if there are include columns SpGistLeafTupleData follow */
 } SpGistSearchItem;
 
 #define SizeOfSpGistSearchItem(n_distances) \
 	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -241,6 +334,7 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
 } SpGistCache;
@@ -321,60 +415,6 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 *(Datum *) SGNTDATAPTR(x) : \
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
-/*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
- *
- * In the simplest case, the datum is the same as the indexed value; but
- * it could also be a suffix or some other sort of delta that permits
- * reconstruction given knowledge of the prefix path traversed to get here.
- *
- * The size field is wider than could possibly be needed for an on-disk leaf
- * tuple, but this allows us to form leaf tuples even when the datum is too
- * wide to be stored immediately, and it costs nothing because of alignment
- * considerations.
- *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
- * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
- * restriction only adds bytes for the null-datum case, otherwise alignment
- * restrictions force it anyway.)
- *
- * In a leaf tuple for a NULL indexed value, there's no useful datum value;
- * however, the SGDTSIZE limit ensures that's there's a Datum word there
- * anyway, so SGLTDATUM can be applied safely as long as you don't do
- * anything with the result.
- */
-typedef struct SpGistLeafTupleData
-{
-	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
-				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
-	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
-} SpGistLeafTupleData;
-
-typedef SpGistLeafTupleData *SpGistLeafTuple;
-
-#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
-#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
-
-/*
- * SPGiST dead tuple: declaration for examining non-live tuples
- *
- * The tupstate field of this struct must match those of regular inner and
- * leaf tuples, and its size field must match a leaf tuple's.
- * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
- * field, to satisfy some Asserts that we make when replacing a leaf tuple
- * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
- * pointer and xid are only valid when tupstate = REDIRECT.
- */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
@@ -394,7 +434,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -456,9 +495,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +506,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +526,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..4fd2b7e878 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -356,7 +356,6 @@ CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..fa64766fb7
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,139 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..a59e73aa22
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,81 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+DROP TABLE tbl_spgist;
+
-- 
2.28.0

#8Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#7)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

With a little bugfix

вт, 11 авг. 2020 г. в 22:50, Pavel Borisov <pashkin.elfe@gmail.com>:

Show quoted text

вт, 11 авг. 2020 г. в 12:11, Pavel Borisov <pashkin.elfe@gmail.com>:

I added changes in documentation into the patch.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com
<http://www.postgrespro.com&gt;

Attachments:

v6-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchapplication/octet-stream; name=v6-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchDownload
From ec819baf34b69ff2a3ba3e882e167ab1d3030b75 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 17 Aug 2020 20:01:48 +0400
Subject: [PATCH v6] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE colums for SPGiST index is intended to increase the speed of queries by making scan index only likewise
in btree and GiST index. These included values are added only to leaf tuples and they are not used in index tree search
but they can be fetched during index scan.

The other point of included columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in
certain cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster update etc. Also there can be included any data types without SP-GiST supported opclasses.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/spgist/README              |   2 +-
 src/backend/access/spgist/spgdoinsert.c       | 172 +++++---
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  87 +++-
 src/backend/access/spgist/spgutils.c          | 385 ++++++++++++++++--
 src/backend/access/spgist/spgvacuum.c         |  25 +-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/spgist_private.h           | 160 +++++---
 src/test/regress/expected/amutils.out         |   4 +-
 src/test/regress/expected/index_including.out |   1 -
 .../expected/index_including_spgist.out       | 139 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 .../regress/sql/index_including_spgist.sql    |  81 ++++
 17 files changed, 901 insertions(+), 185 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 28adaba72d..c89cc6cb08 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index ff87b2d28f..3d360bcf47 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 0e04a08679..868a140a6a 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -240,6 +240,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..87e08431fa 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -73,8 +73,8 @@ Leaf tuple consists of:
     Example:
         radix tree - the rest of string (postfix)
         quad and k-d tree - the point itself
-
   ItemPointer to the heap
+  optional included colums values
 
 
 NULLS HANDLING
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..4c133b7106 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in a chain are
+	 * collected into 2-d arrays: (number of tuples in chain) x (number of
+	 * attributes) First attribute is key, the other - included attributes (if
+	 * any). After picksplit we need to form new leaf tuples as key attribute
+	 * length can change which can affect alignment of every include
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +772,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +799,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +813,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +821,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +834,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	SpGistDeformLeafTuple(newLeafTuple, state, leafChainDatums + (in.nTuples) * natts,
+						  leafChainIsnulls + (in.nTuples) * natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +862,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums + i * natts) = (Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls + i * natts) = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +887,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums + i * natts) = (Datum) 0;
+			*(leafChainIsnulls + i * natts) = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1228,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1921,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum.  Otherwise store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = SpgLeafSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2009,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2043,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2085,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2116,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2158,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2169,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2206,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = SpgLeafSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..5a3c7c50cf 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are include attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add included attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			SpGistDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[0] = leafValue;
+			leafIsnulls[0] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* Included attributes always can be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..9b1633eeda 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -116,14 +127,21 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have included
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +174,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +196,23 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+	/* Form descriptor for included columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
 
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +225,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,7 +639,7 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
+ * Note the result is not maxaligned and this should be done by caller if needed.
  * Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
@@ -619,7 +655,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +678,205 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * at MAXALIGN boundary. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of included
+ * data area in SpGiST leaf tuple. MAXALINGing first include attribute is
+ * avoided for not to introduce unnecessary gap before it.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed for backward
+ * compatibility and there might be gap between header and key data. After key
+ * data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed which is anyway unavoidable
+ * when placing a tuple on a page.
+ */
+unsigned int
+SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for include attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall included attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and included
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to included attributes and is placed just after key data if
+ * there is at least one NULL among included attributes. It doesn't need alignment.
+ * Then all included columns data follow aligned by their typealign's.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are include attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all included attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
 	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add included columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of include attributes tuple is not aligned by default.
+		 * All values alignment should be done by heap_fill_tuple
+		 * automaticaly. If there is a nulls mask it is included just after
+		 * key attribute data and it should not be aligned.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +893,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +940,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1251,133 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of include data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..a0d76901fc 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits has their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..74cb715eaf 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -141,6 +141,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of included columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,6 +149,98 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
+/*
+ * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ *
+ * In the simplest case, the datum is the same as the indexed value; but
+ * it could also be a suffix or some other sort of delta that permits
+ * reconstruction given knowledge of the prefix path traversed to get here.
+ *
+ * The size field is wider than could possibly be needed for an on-disk leaf
+ * tuple, but this allows us to form leaf tuples even when the datum is too
+ * wide to be stored immediately, and it costs nothing because of alignment
+ * considerations.
+ *
+ * Normally, nextOffset links to the next tuple belonging to the same parent
+ * node (which must be on the same page).  But when the root page is a leaf
+ * page, we don't chain its tuples, so nextOffset is always 0 on the root.
+ *
+ * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
+ * so that the tuple can be converted to REDIRECT status later.  (This
+ * restriction only adds bytes for the null-datum case, otherwise alignment
+ * restrictions force it anyway.)
+ *
+ * In a leaf tuple for a NULL indexed value, there's no useful datum value;
+ * however, the SGDTSIZE limit ensures that's there's a Datum word there
+ * anyway, so SGLTDATUM can be applied safely as long as you don't do
+ * anything with the result.
+ *
+ * Minimum space to store SpGistLeafTuple on a page is 12 bytes tuple header
+ * and 4 bytes ItemIdData so 14 lower bits of nextOffset (accessed as
+ * SGLT_GET/SET_OFFSET) is enough to store actual tuple number on a page even
+ * if page size is 64Kb. Two higher bits are to store per-tuple
+ * information is there nulls mask exist and is there any included attribute
+ * of variable length type.
+ */
+
+typedef struct SpGistLeafTupleData
+{
+	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
+				size:30;		/* large enough for any palloc'able value */
+	OffsetNumber nextOffset;	/* higher 1 bit = 1 if included values has
+								 * nulls, 2 bit = 1 if included values contain
+								 * variable length values, lower 15 bits - is
+								 * "actual" nextOffset i.e. number of next
+								 * tuple in chain on a page, or
+								 * InvalidOffsetNumber. They SHOULD NOT be
+								 * set/read directly,
+								 * SGLT_SET_XXX/SGLT_GET_XXX macros must be
+								 * used instead. */
+	ItemPointerData heapPtr;	/* TID of represented heap tuple */
+	/* leaf datum follows */
+
+	/*
+	 * if SGLT_GET_CONTAINSNULLMASK nullmask follows. Its size (number of
+	 * included columns/8)+1
+	 */
+	/* include attributes follow if any */
+} SpGistLeafTupleData;
+
+typedef SpGistLeafTupleData *SpGistLeafTuple;
+
+#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
+#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
+#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Accessor macros to get and set actual 14-bit offset and two bit flags from/to
+ * nextOffset value.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 0x4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
+
+#define SGLT_GET_INCLUDE_TUPSIZE(x) SGLT_GET_OFFSET(x)
+#define SGLT_SET_INCLUDE_TUPSIZE(x,o) SGLT_SET_OFFSET(x,o)
+
+extern char *SpGistFormIncludeTuple(TupleDesc tupleDescriptor, Datum *values,
+									bool *isnull, uint16 *tupdata);
+
+/*
+ * SPGiST dead tuple: declaration for examining non-live tuples
+ *
+ * The tupstate field of this struct must match those of regular inner and
+ * leaf tuples, and its size field must match a leaf tuple's.
+ * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
+ * field, to satisfy some Asserts that we make when replacing a leaf tuple
+ * with a dead tuple.
+ * We don't use nextOffset, but it's needed to align the pointer field.
+ */
+
 typedef struct SpGistSearchItem
 {
 	pairingheap_node phNode;	/* pairing heap node */
@@ -160,14 +253,14 @@ typedef struct SpGistSearchItem
 	bool		isLeaf;			/* SearchItem is heap item */
 	bool		recheck;		/* qual recheck is needed */
 	bool		recheckDistances;	/* distance recheck is needed */
-
+	SpGistLeafTuple leafTuple;
 	/* array with numberOfOrderBys entries */
 	double		distances[FLEXIBLE_ARRAY_MEMBER];
+	/* if there are include columns SpGistLeafTupleData follow */
 } SpGistSearchItem;
 
 #define SizeOfSpGistSearchItem(n_distances) \
 	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -241,6 +334,7 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
 } SpGistCache;
@@ -321,60 +415,6 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 *(Datum *) SGNTDATAPTR(x) : \
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
-/*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
- *
- * In the simplest case, the datum is the same as the indexed value; but
- * it could also be a suffix or some other sort of delta that permits
- * reconstruction given knowledge of the prefix path traversed to get here.
- *
- * The size field is wider than could possibly be needed for an on-disk leaf
- * tuple, but this allows us to form leaf tuples even when the datum is too
- * wide to be stored immediately, and it costs nothing because of alignment
- * considerations.
- *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
- * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
- * restriction only adds bytes for the null-datum case, otherwise alignment
- * restrictions force it anyway.)
- *
- * In a leaf tuple for a NULL indexed value, there's no useful datum value;
- * however, the SGDTSIZE limit ensures that's there's a Datum word there
- * anyway, so SGLTDATUM can be applied safely as long as you don't do
- * anything with the result.
- */
-typedef struct SpGistLeafTupleData
-{
-	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
-				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
-	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
-} SpGistLeafTupleData;
-
-typedef SpGistLeafTupleData *SpGistLeafTuple;
-
-#define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
-#define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
-
-/*
- * SPGiST dead tuple: declaration for examining non-live tuples
- *
- * The tupstate field of this struct must match those of regular inner and
- * leaf tuples, and its size field must match a leaf tuple's.
- * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
- * field, to satisfy some Asserts that we make when replacing a leaf tuple
- * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
- * pointer and xid are only valid when tupstate = REDIRECT.
- */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
@@ -394,7 +434,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -456,9 +495,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +506,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +526,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..4fd2b7e878 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -356,7 +356,6 @@ CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..fa64766fb7
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,139 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..a59e73aa22
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,81 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+DROP TABLE tbl_spgist;
+
-- 
2.28.0

#9Andrey M. Borodin
x4mmm@yandex-team.ru
In reply to: Pavel Borisov (#8)
Re: [PATCH] Covering SPGiST index

Hi!

17 авг. 2020 г., в 21:04, Pavel Borisov <pashkin.elfe@gmail.com> написал(а):

Postgres Professional: http://postgrespro.com
<v6-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patch>

I'm looking into the patch. I have few notes:

1. I see that in src/backend/access/spgist/README you describe SP-GiST tuple as sequence of {Value, ItemPtr to heap, Included attributes}. Is it different from regular IndexTuple where tid is within TupleHeader?

2. Instead of cluttering tuple->nextOffset with bit flags we could just change Tuple Header for leaf tuples with covering indexes. Interpret tuples for indexes with included attributes differently, iff it makes code cleaner. There are so many changes with SGLT_SET_OFFSET\SGLT_GET_OFFSET that it seems viable to put some effort into research of other ways to represent two bits for null mask and varatts.

3. Comment "* SPGiST dead tuple: declaration for examining non-live tuples" does not precede relevant code. because struct SpGistDeadTupleData was not moved.

Thanks!

Best regards, Andrey Borodin.

#10Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Andrey M. Borodin (#9)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

I'm looking into the patch. I have few notes:

1. I see that in src/backend/access/spgist/README you describe SP-GiST
tuple as sequence of {Value, ItemPtr to heap, Included attributes}. Is it
different from regular IndexTuple where tid is within TupleHeader?

Yes, the header of SpGist tuple is put down in a little bit different way
than index tuple. It is also intended to connect spgist leaf tuples in
chains on a leaf page so it already have more complex layout and bigger
size that index tuple header.

SpGist tuple header size is 12 bytes which is a maxaligned value for 32 bit
architectures, and key value can start just after it without any gap. This
is of value, as unnecessary index size increase slows down performance and
is evil anyway. The only part of this which is left now is a gap
between SpGist tuple header and first value on 64 bit architecture (as
maxalign value in this case is 16 bytes and 4 bytes per tuple can be
saved). But I was discouraged to change this on the reason of binary
compatibility with indexes built before and complexity of the change also,
as quite many things in the code do depend on this maxaligned header (for
inner and dead tuples also).

Another difference is that SpGist nulls mask is inserted after the key
value before the first included one and apply only to included values. It
is not needed for key values, as null key values in SpGist are stored in
separate tree, and it is not needed to mark it null second time. Also nulls
mask size in Spgist does depend on the number of included values in a
tuple, unlike in IndexTuple which contains redundant nulls mask for all
possible INDEX_MAX_KEYS. In certain cases we can store nulls mask in free
bytes after key value before typealign of first included value. (E.g. if
key value is varchar (radix tree) statistically we have only 1/8 of keys
finishing exactly an maxalign, the others will have a natural gap for nulls
mask.)

2. Instead of cluttering tuple->nextOffset with bit flags we could just

change Tuple Header for leaf tuples with covering indexes. Interpret tuples
for indexes with included attributes differently, iff it makes code
cleaner. There are so many changes with SGLT_SET_OFFSET\SGLT_GET_OFFSET
that it seems viable to put some effort into research of other ways to
represent two bits for null mask and varatts.

Of course SpGist header can be done different for index with and without
included columns. I see two reasons against this:
1. It will be needed to integrate many ifs and in many places keep in mind
whether the index contains included values. It is expected to be much more
code than now and not only in the parts which integrates included values to
leaf tuples. I think this vast changes can puzzle reader much more than
just two small macros evenly copy-pasted in the code.
2. I also see no need to increase SpGist tuple size just for inserting two
bits which are now stored free of charge. I consulted with bit flags
storage in IndexTupleData.t_tid and did it in a similar way. Macros for
GET/SET are basically needed to make bit flags and offset modification
independent and safe in any place of a code.

I added some extra comments and mentions in manual to make all the things
clear (see v7 patch)

3. Comment "* SPGiST dead tuple: declaration for examining non-live
tuples" does not precede relevant code. because struct SpGistDeadTupleData
was not moved.

You are right, thank you! Corrected this and also removed some unnecessary
declarations.

Thank you for your attention to the patch!

Attachments:

v7-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchapplication/octet-stream; name=v7-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchDownload
From 083e2c691a912fc1555626f80b49c02f4c4aaf11 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 24 Aug 2020 17:17:31 +0400
Subject: [PATCH v7] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE colums for SPGiST index is intended to increase the speed of queries by making scan index only likewise
in btree and GiST index. These included values are added only to leaf tuples and they are not used in index tree search
but they can be fetched during index scan.

The other point of included columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in
certain cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster update etc. Also there can be included any data types without SP-GiST supported opclasses.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/spgist/README              |  18 +-
 src/backend/access/spgist/spgdoinsert.c       | 172 +++++---
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  87 +++-
 src/backend/access/spgist/spgutils.c          | 385 ++++++++++++++++--
 src/backend/access/spgist/spgvacuum.c         |  25 +-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/spgist_private.h           | 265 +++++++-----
 src/test/regress/expected/amutils.out         |   4 +-
 src/test/regress/expected/index_including.out |   1 -
 .../expected/index_including_spgist.out       | 139 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 .../regress/sql/index_including_spgist.sql    |  80 ++++
 17 files changed, 968 insertions(+), 238 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 28adaba72d..c89cc6cb08 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index ff87b2d28f..3d360bcf47 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 0e04a08679..868a140a6a 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -240,6 +240,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..636747a6a8 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -73,9 +73,20 @@ Leaf tuple consists of:
     Example:
         radix tree - the rest of string (postfix)
         quad and k-d tree - the point itself
-
   ItemPointer to the heap
-
+  nextOffset number that points to next leaf tuple in a chain
+  optional nullmask for included column values
+  optional included colums values
+
+Parts of leaf tuple are laid out to make the header and the key value
+placement unchanged in case of index with and without included values and
+backward compatible. Also it is intended to be aligned with minimum possible
+gaps to make index smaller. I.e. first header of 12 bytes, then a key value
+starting from maxalign boundary, then just immediately nulls mask bytes,
+then included attributes each starting from its typealign boundary. So in
+many cases nulls mask is stored free of charge and tuple occupy minimum
+possible space (with exception of gap before key value which starts from
+maxalign for compatibility).
 
 NULLS HANDLING
 
@@ -90,6 +101,9 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For included attributes nulls are handled in ordinary per leaf-tuple way i.e.
+there is null mask presence bit in a header and, if it is true, nullmask is
+added just after key value before the first included attribute.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..4c133b7106 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in a chain are
+	 * collected into 2-d arrays: (number of tuples in chain) x (number of
+	 * attributes) First attribute is key, the other - included attributes (if
+	 * any). After picksplit we need to form new leaf tuples as key attribute
+	 * length can change which can affect alignment of every include
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +772,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +799,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				SpGistDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +813,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +821,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +834,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	SpGistDeformLeafTuple(newLeafTuple, state, leafChainDatums + (in.nTuples) * natts,
+						  leafChainIsnulls + (in.nTuples) * natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +862,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums + i * natts) = (Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls + i * natts) = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +887,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums + i * natts) = (Datum) 0;
+			*(leafChainIsnulls + i * natts) = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1228,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1921,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum.  Otherwise store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = SpgLeafSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2009,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2043,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2085,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2116,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2158,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2169,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2206,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = SpgLeafSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..5a3c7c50cf 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are include attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add included attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			SpGistDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[0] = leafValue;
+			leafIsnulls[0] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* Included attributes always can be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..9b1633eeda 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -116,14 +127,21 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have included
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +174,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +196,23 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+	/* Form descriptor for included columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
 
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +225,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,7 +639,7 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
+ * Note the result is not maxaligned and this should be done by caller if needed.
  * Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
@@ -619,7 +655,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +678,205 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * at MAXALIGN boundary. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of included
+ * data area in SpGiST leaf tuple. MAXALINGing first include attribute is
+ * avoided for not to introduce unnecessary gap before it.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed for backward
+ * compatibility and there might be gap between header and key data. After key
+ * data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed which is anyway unavoidable
+ * when placing a tuple on a page.
+ */
+unsigned int
+SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for include attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall included attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and included
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to included attributes and is placed just after key data if
+ * there is at least one NULL among included attributes. It doesn't need alignment.
+ * Then all included columns data follow aligned by their typealign's.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are include attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all included attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
 	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add included columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of include attributes tuple is not aligned by default.
+		 * All values alignment should be done by heap_fill_tuple
+		 * automaticaly. If there is a nulls mask it is included just after
+		 * key attribute data and it should not be aligned.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +893,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +940,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1251,133 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of include data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..a0d76901fc 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits has their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..f55549fada 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,7 +22,6 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
@@ -141,6 +140,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of included columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,104 +148,6 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
-/*
- * Private state of an index scan
- */
-typedef struct SpGistScanOpaqueData
-{
-	SpGistState state;			/* see above */
-	pairingheap *scanQueue;		/* queue of to be visited items */
-	MemoryContext tempCxt;		/* short-lived memory context */
-	MemoryContext traversalCxt; /* single scan lifetime memory context */
-
-	/* Control flags showing whether to search nulls and/or non-nulls */
-	bool		searchNulls;	/* scan matches (all) null entries */
-	bool		searchNonNulls; /* scan matches (some) non-null entries */
-
-	/* Index quals to be passed to opclass (null-related quals removed) */
-	int			numberOfKeys;	/* number of index qualifier conditions */
-	ScanKey		keyData;		/* array of index qualifier descriptors */
-	int			numberOfOrderBys;	/* number of ordering operators */
-	int			numberOfNonNullOrderBys;	/* number of ordering operators
-											 * with non-NULL arguments */
-	ScanKey		orderByData;	/* array of ordering op descriptors */
-	Oid		   *orderByTypes;	/* array of ordering op return types */
-	int		   *nonNullOrderByOffsets;	/* array of offset of non-NULL
-										 * ordering keys in the original array */
-	Oid			indexCollation; /* collation of index column */
-
-	/* Opclass defined functions: */
-	FmgrInfo	innerConsistentFn;
-	FmgrInfo	leafConsistentFn;
-
-	/* Pre-allocated workspace arrays: */
-	double	   *zeroDistances;
-	double	   *infDistances;
-
-	/* These fields are only used in amgetbitmap scans: */
-	TIDBitmap  *tbm;			/* bitmap being filled */
-	int64		ntids;			/* number of TIDs passed to bitmap */
-
-	/* These fields are only used in amgettuple scans: */
-	bool		want_itup;		/* are we reconstructing tuples? */
-	TupleDesc	indexTupDesc;	/* if so, tuple descriptor for them */
-	int			nPtrs;			/* number of TIDs found on current page */
-	int			iPtr;			/* index for scanning through same */
-	ItemPointerData heapPtrs[MaxIndexTuplesPerPage];	/* TIDs from cur page */
-	bool		recheck[MaxIndexTuplesPerPage]; /* their recheck flags */
-	bool		recheckDistances[MaxIndexTuplesPerPage];	/* distance recheck
-															 * flags */
-	HeapTuple	reconTups[MaxIndexTuplesPerPage];	/* reconstructed tuples */
-
-	/* distances (for recheck) */
-	IndexOrderByDistance *distances[MaxIndexTuplesPerPage];
-
-	/*
-	 * Note: using MaxIndexTuplesPerPage above is a bit hokey since
-	 * SpGistLeafTuples aren't exactly IndexTuples; however, they are larger,
-	 * so this is safe.
-	 */
-} SpGistScanOpaqueData;
-
-typedef SpGistScanOpaqueData *SpGistScanOpaque;
-
-/*
- * This struct is what we actually keep in index->rd_amcache.  It includes
- * static configuration information as well as the lastUsedPages cache.
- */
-typedef struct SpGistCache
-{
-	spgConfigOut config;		/* filled in by opclass config method */
-
-	SpGistTypeDesc attType;		/* type of values to be indexed/restored */
-	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
-	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
-	SpGistTypeDesc attLabelType;	/* type of node label values */
-
-	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
-} SpGistCache;
-
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +207,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,11 +224,13 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a key datum, a heap tuple TID and optional
+ * datums and nullmask of included columns.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of included columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
@@ -346,14 +250,44 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Minimum space to store SpGistLeafTuple plus ItemIdData on a page is 16 bytes,
+ * so 14 lower bits of nextOffset is enough to store tuple number in a chain
+ * on a page even if page size is 64Kb. Two higher bits are to store per-tuple
+ * information for included attributes: is there nulls mask exist, and is there
+ * any included attribute of variable length type. If there are no included
+ * columns these higher bits are not used.
+ *
+ * If there are included columns, they are stored after a key value each starting
+ * from its own typalign boundary. Unlike IndexTuple, first included value does
+ * not need to be stored, starting from MAXALIGN boundary, and SPGiST uses
+ * private routines to access them. Nullmask with size of
+ * (number of included columns)/8 bytes is put without alignment between key
+ * and first included column. If there is an alignment gap between them,
+ * nullmask has a good chance to fit into the gap, thus making its storage free of
+ * charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * nextOffset is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: included values has nulls
+	 * 14th bit: included values has var-length attributes
+	 * 13-0 bit: number of next tuple in chain on a page, or InvalidOffsetNumber
+	 * ---------------
+	 */
+
+	unsigned short nextOffset;	/* info for linking tuples in a chain on a leaf page,
+								   and additional info for included attributes */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* key column data follows */
+	/* nullmask of included values follows if there are nulls in included attributes*/
+	/* included columns data follow if any */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
@@ -361,8 +295,17 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
 #define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
 #define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
 #define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Macros to access bit fields inside nextOffset independently.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 0x4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -373,7 +316,6 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
  * field, to satisfy some Asserts that we make when replacing a leaf tuple
  * with a dead tuple.
  * We don't use nextOffset, but it's needed to align the pointer field.
- * pointer and xid are only valid when tupstate = REDIRECT.
  */
 typedef struct SpGistDeadTupleData
 {
@@ -394,7 +336,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +351,105 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+	/* if there are include columns SpGistLeafTupleData follow */
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+/*
+ * Private state of an index scan
+ */
+typedef struct SpGistScanOpaqueData
+{
+	SpGistState state;			/* see above */
+	pairingheap *scanQueue;		/* queue of to be visited items */
+	MemoryContext tempCxt;		/* short-lived memory context */
+	MemoryContext traversalCxt; /* single scan lifetime memory context */
+
+	/* Control flags showing whether to search nulls and/or non-nulls */
+	bool		searchNulls;	/* scan matches (all) null entries */
+	bool		searchNonNulls; /* scan matches (some) non-null entries */
+
+	/* Index quals to be passed to opclass (null-related quals removed) */
+	int			numberOfKeys;	/* number of index qualifier conditions */
+	ScanKey		keyData;		/* array of index qualifier descriptors */
+	int			numberOfOrderBys;	/* number of ordering operators */
+	int			numberOfNonNullOrderBys;	/* number of ordering operators
+											 * with non-NULL arguments */
+	ScanKey		orderByData;	/* array of ordering op descriptors */
+	Oid		   *orderByTypes;	/* array of ordering op return types */
+	int		   *nonNullOrderByOffsets;	/* array of offset of non-NULL
+										 * ordering keys in the original array */
+	Oid			indexCollation; /* collation of index column */
+
+	/* Opclass defined functions: */
+	FmgrInfo	innerConsistentFn;
+	FmgrInfo	leafConsistentFn;
+
+	/* Pre-allocated workspace arrays: */
+	double	   *zeroDistances;
+	double	   *infDistances;
+
+	/* These fields are only used in amgetbitmap scans: */
+	TIDBitmap  *tbm;			/* bitmap being filled */
+	int64		ntids;			/* number of TIDs passed to bitmap */
+
+	/* These fields are only used in amgettuple scans: */
+	bool		want_itup;		/* are we reconstructing tuples? */
+	TupleDesc	indexTupDesc;	/* if so, tuple descriptor for them */
+	int			nPtrs;			/* number of TIDs found on current page */
+	int			iPtr;			/* index for scanning through same */
+	ItemPointerData heapPtrs[MaxIndexTuplesPerPage];	/* TIDs from cur page */
+	bool		recheck[MaxIndexTuplesPerPage]; /* their recheck flags */
+	bool		recheckDistances[MaxIndexTuplesPerPage];	/* distance recheck
+															 * flags */
+	HeapTuple	reconTups[MaxIndexTuplesPerPage];	/* reconstructed tuples */
+
+	/* distances (for recheck) */
+	IndexOrderByDistance *distances[MaxIndexTuplesPerPage];
+
+	/*
+	 * Note: using MaxIndexTuplesPerPage above is a bit hokey since
+	 * SpGistLeafTuples aren't exactly IndexTuples; however, they are larger,
+	 * so this is safe.
+	 */
+} SpGistScanOpaqueData;
+
+typedef SpGistScanOpaqueData *SpGistScanOpaque;
+
+/*
+ * This struct is what we actually keep in index->rd_amcache.  It includes
+ * static configuration information as well as the lastUsedPages cache.
+ */
+typedef struct SpGistCache
+{
+	spgConfigOut config;		/* filled in by opclass config method */
+
+	SpGistTypeDesc attType;		/* type of values to be indexed/restored */
+	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
+	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
+	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
+
+	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+} SpGistCache;
+
 /*
  * XLOG stuff
  */
@@ -456,9 +496,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int SpgLeafSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +507,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void SpGistDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +527,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..4fd2b7e878 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -356,7 +356,6 @@ CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..fa64766fb7
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,139 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..c47f713d25
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,80 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+DROP TABLE tbl_spgist;
-- 
2.28.0

#11Anastasia Lubennikova
a.lubennikova@postgrespro.ru
In reply to: Pavel Borisov (#10)
Re: [PATCH] Covering SPGiST index

On 24.08.2020 16:19, Pavel Borisov wrote:

I added some extra comments and mentions in manual to make all the
things clear (see v7 patch)

The patch implements the proposed functionality, passes tests, and in
general looks good to me.
I don't mind using a macro to differentiate tuples with and without
included attributes. Any approach will require code changes. Though, I
don't have a strong opinion about that.

A bit of nitpicking:

1) You mention backward compatibility in some comments. But, after this
patch will be committed, it will be uneasy to distinct new and old
phrases.  So I suggest to rephrase them.  You can either refer a
specific version or just call it "compatibility with indexes without
included attributes".

2) SpgLeafSize() function name seems misleading, as it actually refers
to a tuple's size, not a leaf page. I suggest to rename it to
SpgLeafTupleSize().

3) I didn't quite get the meaning of the assertion, that is added in a
few places:
     Assert(so->state.includeTupdesc->natts);
Should it be Assert(so->state.includeTupdesc->natts > 1) ?

4) There are a few typos in comments and docs:
s/colums/columns
s/include attribute/included attribute

and so on.

5) This comment in index_including.sql is outdated:
  * 7. Check various AMs. All but btree and gist must fail.

6) New test lacks SET enable_seqscan TO off;
in addition to SET enable_bitmapscan TO off;

I also wonder, why both index_including_spgist.sql and
index_including.sql tests are stable without running 'vacuum analyze'
before the EXPLAIN that shows Index Only Scan plan. Is autovacuum just
always fast enough to fill a visibility map, or I miss something?

--
Anastasia Lubennikova
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#12Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Anastasia Lubennikova (#11)
2 attachment(s)
Re: [PATCH] Covering SPGiST index

3) I didn't quite get the meaning of the assertion, that is added in a few
places:
Assert(so->state.includeTupdesc->natts);
Should it be Assert(so->state.includeTupdesc->natts > 1) ?

It is rather Assert(so->state.includeTupdesc->natts > 0) as INCLUDE tuple
descriptor should not be initialized and filled in case of index without
INCLUDE attributes and doesn't contain any info about key attribute which
is processed by SpGist existing way separately for different SpGist tuple
types i.e. leaf, prefix=inner and label tuples. So only INCLUDE attributes
are counted there. This and similar Asserts are for the case includeTupdesc
becomes mistakenly initialized by some future code change.

I completely agree with all the other suggestions and made corrections (see
v8). Thank you very much for your review!
Also there is a separate patch 0002 to add VACUUM ANALYZE to
index_including test which is not necessary for covering spgist.

One more point to note: in spgist_private.h I needed to shift down whole
block between
*"typedef struct SpGistSearchItem"*
*and *
*"} SpGistCache;"*
to position it below tuples types declarations to insert pointer
"SpGistLeafTuple leafTuple"; into struct SpGistSearchItem. This is the only
change in this block and I apologize for possible inconvenience to review
this change.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v8-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchapplication/octet-stream; name=v8-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchDownload
From 0f8140f874db2a5786ccbc3a67b3910722f59f5b Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 27 Aug 2020 19:37:44 +0400
Subject: [PATCH v8] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE columns for SPGiST index is intended to increase the speed of queries by making scans index-only likewise
in btree and GiST index. These columns are added only to leaf tuples and they are not used in index tree search but they
can be fetched during index scan.

The other point of INCLUDE columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in certain
cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster, update etc. Also, any data types without SP-GiST supported opclasses can be included.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/spgist/README              |  21 +-
 src/backend/access/spgist/spgdoinsert.c       | 172 +++++---
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  87 +++-
 src/backend/access/spgist/spgutils.c          | 389 ++++++++++++++++--
 src/backend/access/spgist/spgvacuum.c         |  25 +-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/spgist_private.h           | 263 +++++++-----
 src/test/regress/expected/amutils.out         |   4 +-
 src/test/regress/expected/index_including.out |   3 +-
 .../expected/index_including_spgist.out       | 143 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 src/test/regress/sql/index_including.sql      |   2 +-
 .../regress/sql/index_including_spgist.sql    |  84 ++++
 18 files changed, 982 insertions(+), 241 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 28adaba72d..c89cc6cb08 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index ff87b2d28f..3d360bcf47 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 0e04a08679..868a140a6a 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -240,6 +240,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..55b515f03d 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -73,9 +73,22 @@ Leaf tuple consists of:
     Example:
         radix tree - the rest of string (postfix)
         quad and k-d tree - the point itself
-
   ItemPointer to the heap
-
+  nextOffset number of next leaf tuple in a chain on a leaf page
+  optional nullmask for INCLUDE columns
+  optional INCLUDE columns values
+
+Leaf tuple layout changed since PostgreSQL version 14 to support INCLUDE
+columns but in a way that doesn't change the header and the key value
+placement in a tuple. So indexes created earlier remain fully supported.
+
+Also it is intended to be laid out with minimum possible gaps to make index
+smaller. I.e. first header of 12 bytes, then a key value starting from
+maxalign boundary, then just immediately nulls mask bytes, then INCLUDE
+attributes each starting from its typealign boundary. So in many cases,
+nullmask is stored free of charge and tuple occupy minimum possible space
+(with exception of gap before key value which starts from maxalign for
+compatibility).
 
 NULLS HANDLING
 
@@ -90,6 +103,10 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For INCLUDE attributes nulls are handled in ordinary per leaf-tuple way i.e.
+if null mask presence bit in a header is set, nullmask is added just after
+key value before the first INCLUDE attribute. Note that nullmask presence
+bit and nullmask itself apply only to INCLUDE attributes.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..a5994c7100 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, SGLT_GET_OFFSET(head->nextOffset));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head->nextOffset, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it->nextOffset);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it->nextOffset, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple->nextOffset, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,9 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
 
 	in.level = level;
 
@@ -723,14 +726,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +743,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in the chain are
+	 * collected into 2-d arrays: (number of tuples in the chain) x (number of
+	 * attributes) The first attribute is key, the other - INCLUDE attributes (if
+	 * any). After picksplit we need to form new leaf tuples as the key attribute
+	 * length can change which can affect the alignment of every INCLUDE
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +772,8 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				spgDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +799,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+
+				spgDeformLeafTuple(it, state, leafChainDatums + nToInsert * natts,
+									  leafChainIsnulls + nToInsert * natts, isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +813,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it->nextOffset) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +821,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it->nextOffset);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +834,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+
+	spgDeformLeafTuple(newLeafTuple, state, leafChainDatums + (in.nTuples) * natts,
+						  leafChainIsnulls + (in.nTuples) * natts, isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +862,11 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			*(leafChainDatums + i * natts) = (Datum) out.leafTupleDatums[i];
+			*(leafChainIsnulls + i * natts) = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +887,14 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			*(leafChainDatums + i * natts) = (Datum) 0;
+			*(leafChainIsnulls + i * natts) = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i, leafChainDatums + i * natts,
+										   leafChainIsnulls + i * natts);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1228,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it->nextOffset, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it->nextOffset, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1921,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum. Otherwise, store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[0])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[0] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[0]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[0] = PointerGetDatum(PG_DETOAST_DATUM(datum[0]));
 			else
-				leafDatum = datum;
+				leafDatum[0] = datum[0];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[0] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = spgLeafTupleSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2009,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[0] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2043,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[0] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2085,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[0] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[0], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2116,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[0]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[0], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2158,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[0];
+			in.leafDatum = leafDatum[0];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2169,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[0])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2206,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[0])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[0] = out.result.matchNode.restDatum;
+						leafSize = spgLeafTupleSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..fbf8bd5435 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are INCLUDE attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple->nextOffset) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple->nextOffset);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add INCLUDE attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			spgDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[0] = leafValue;
+			leafIsnulls[0] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* INCLUDE attributes can always be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..5247c5b4b0 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -116,14 +127,21 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have INCLUDE
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +174,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +196,23 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+	/* Form descriptor for INCLUDE columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
 
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +225,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,8 +639,8 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
- * Also, we follow the SPGiST convention that pass-by-val types are
+ * Note the result is not maxaligned and this should be done by the caller if
+ * needed. Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
 unsigned int
@@ -619,7 +655,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +678,205 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * at MAXALIGN boundary. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of INCLUDE
+ * data area in SpGiST leaf tuple. MAXALINGing first INCLUDE attribute is
+ * avoided for not to introduce unnecessary gap before it.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed for backward
+ * compatibility and there might be a gap between header and key data. After
+ * key data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed which is anyway unavoidable
+ * when placing a tuple on a page.
+ */
+unsigned int
+spgLeafTupleSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for INCLUDE attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall INCLUDE attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and INCLUDE
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to INCLUDE attributes and is placed just after key data if
+ * there is at least one NULL among INCLUDE attributes. It doesn't need alignment.
+ * Then all INCLUDE columns data follow aligned by their typealign-s.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are INCLUDE attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[0])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all INCLUDE attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
-	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * Ensure that we can replace the tuple with a dead tuple later. This
+	 * test is unnecessary when !isnull[0], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup->nextOffset, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[0])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[0]);
+
+	/* Add INCLUDE columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of INCLUDE attributes tuple (include_offset) is next
+		 * byte after end of a key value and is not required to be aligned.
+		 * Nullmask is included without alignment and values alignment are
+		 * done by heap_fill_tuple() automatically.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup->nextOffset, 1);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup->nextOffset, 1);
+	}
 	return tup;
 }
 
@@ -688,10 +893,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +940,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1251,133 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+spgDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of INCLUDE data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[0] = (Datum) 0;
+		isnull[0] = true;
+	}
+	else
+	{
+		datum[0] = SGLTDATUM(tup, state);
+		isnull[0] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[0]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup->nextOffset) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup->nextOffset))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..badda5f9e0 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt->nextOffset) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt->nextOffset) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt->nextOffset) > max ||
+					predecessor[SGLT_GET_OFFSET(lt->nextOffset)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt->nextOffset)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits have their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt->nextOffset) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt->nextOffset) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head->nextOffset);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt->nextOffset);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..4022e3af07 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head->nextOffset) == SGLT_GET_OFFSET(leafTupleHdr.nextOffset));
+				SGLT_SET_OFFSET(head->nextOffset, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt->nextOffset, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..75e09f6664 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,7 +22,6 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
@@ -141,6 +140,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of INCLUDE columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,104 +148,6 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
-/*
- * Private state of an index scan
- */
-typedef struct SpGistScanOpaqueData
-{
-	SpGistState state;			/* see above */
-	pairingheap *scanQueue;		/* queue of to be visited items */
-	MemoryContext tempCxt;		/* short-lived memory context */
-	MemoryContext traversalCxt; /* single scan lifetime memory context */
-
-	/* Control flags showing whether to search nulls and/or non-nulls */
-	bool		searchNulls;	/* scan matches (all) null entries */
-	bool		searchNonNulls; /* scan matches (some) non-null entries */
-
-	/* Index quals to be passed to opclass (null-related quals removed) */
-	int			numberOfKeys;	/* number of index qualifier conditions */
-	ScanKey		keyData;		/* array of index qualifier descriptors */
-	int			numberOfOrderBys;	/* number of ordering operators */
-	int			numberOfNonNullOrderBys;	/* number of ordering operators
-											 * with non-NULL arguments */
-	ScanKey		orderByData;	/* array of ordering op descriptors */
-	Oid		   *orderByTypes;	/* array of ordering op return types */
-	int		   *nonNullOrderByOffsets;	/* array of offset of non-NULL
-										 * ordering keys in the original array */
-	Oid			indexCollation; /* collation of index column */
-
-	/* Opclass defined functions: */
-	FmgrInfo	innerConsistentFn;
-	FmgrInfo	leafConsistentFn;
-
-	/* Pre-allocated workspace arrays: */
-	double	   *zeroDistances;
-	double	   *infDistances;
-
-	/* These fields are only used in amgetbitmap scans: */
-	TIDBitmap  *tbm;			/* bitmap being filled */
-	int64		ntids;			/* number of TIDs passed to bitmap */
-
-	/* These fields are only used in amgettuple scans: */
-	bool		want_itup;		/* are we reconstructing tuples? */
-	TupleDesc	indexTupDesc;	/* if so, tuple descriptor for them */
-	int			nPtrs;			/* number of TIDs found on current page */
-	int			iPtr;			/* index for scanning through same */
-	ItemPointerData heapPtrs[MaxIndexTuplesPerPage];	/* TIDs from cur page */
-	bool		recheck[MaxIndexTuplesPerPage]; /* their recheck flags */
-	bool		recheckDistances[MaxIndexTuplesPerPage];	/* distance recheck
-															 * flags */
-	HeapTuple	reconTups[MaxIndexTuplesPerPage];	/* reconstructed tuples */
-
-	/* distances (for recheck) */
-	IndexOrderByDistance *distances[MaxIndexTuplesPerPage];
-
-	/*
-	 * Note: using MaxIndexTuplesPerPage above is a bit hokey since
-	 * SpGistLeafTuples aren't exactly IndexTuples; however, they are larger,
-	 * so this is safe.
-	 */
-} SpGistScanOpaqueData;
-
-typedef SpGistScanOpaqueData *SpGistScanOpaque;
-
-/*
- * This struct is what we actually keep in index->rd_amcache.  It includes
- * static configuration information as well as the lastUsedPages cache.
- */
-typedef struct SpGistCache
-{
-	spgConfigOut config;		/* filled in by opclass config method */
-
-	SpGistTypeDesc attType;		/* type of values to be indexed/restored */
-	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
-	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
-	SpGistTypeDesc attLabelType;	/* type of node label values */
-
-	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
-} SpGistCache;
-
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +207,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,11 +224,13 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a key datum, a heap tuple TID and optional
+ * datums and nullmask of INCLUDE columns.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of INCLUDE columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
@@ -346,14 +250,43 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Minimum space to store SpGistLeafTuple plus ItemIdData on a page is 16 bytes,
+ * so 14 lower bits of nextOffset is enough to store tuple number in a chain
+ * on a page even if a page size is 64Kb. Two higher bits are to store per-tuple
+ * information for INCLUDE attributes: is there nulls mask exist, and are there
+ * any INCLUDE attributes of variable length type. If there are no INCLUDE
+ * columns these higher bits are not used.
+ *
+ * If there are INCLUDE columns, they are stored after a key value, each
+ * starting from its own typalign boundary. Unlike IndexTuple, first INCLUDE
+ * value does not need to start from MAXALIGN boundary, so SPGiST uses private
+ * routines to access them. Nullmask with size (number of INCLUDE columns)/8
+ * bytes is put without alignment between the key and the first INCLUDE column.
+ * If there is an alignment gap between them, nullmask has a good chance to fit
+ * into the gap, thus making its storage free of charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * nextOffset is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: INCLUDE values has nulls
+	 * 14th bit: INCLUDE values has var-length attributes
+	 * 13-0 bit: number of next tuple in chain on a page, or InvalidOffsetNumber
+	 * ---------------
+	 */
+
+	unsigned short nextOffset;	/* info for linking tuples in a chain on a leaf page,
+								   and additional info for INCLUDE attributes */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* key column data follows */
+	/* nullmask of INCLUDE values follows if there are nulls in INCLUDE attributes*/
+	/* INCLUDE columns data follow if any */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
@@ -361,8 +294,17 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
 #define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
 #define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
 #define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Macros to access bit fields inside nextOffset independently.
+ */
+#define SGLT_GET_OFFSET(x) 	( (x) & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(x) ( (x) >> 15 )
+#define SGLT_GET_CONTAINSVARATT(x) ( ( (x) & 0x4000 ) >> 14 )
+#define SGLT_SET_OFFSET(x,o) ( (x) = ( (x) & 0xC000 ) | ( (o) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(x,n) ( (x) = ( (n) << 15 ) | ( (x) & 0x3FFF ) )
+#define SGLT_SET_CONTAINSVARATT(x,v) ( (x) = ( (v) << 14 ) | ( (x) & 0xBFFF ) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -394,7 +336,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +351,105 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+
+/*
+ * Private state of an index scan
+ */
+typedef struct SpGistScanOpaqueData
+{
+	SpGistState state;			/* see above */
+	pairingheap *scanQueue;		/* queue of to be visited items */
+	MemoryContext tempCxt;		/* short-lived memory context */
+	MemoryContext traversalCxt; /* single scan lifetime memory context */
+
+	/* Control flags showing whether to search nulls and/or non-nulls */
+	bool		searchNulls;	/* scan matches (all) null entries */
+	bool		searchNonNulls; /* scan matches (some) non-null entries */
+
+	/* Index quals to be passed to opclass (null-related quals removed) */
+	int			numberOfKeys;	/* number of index qualifier conditions */
+	ScanKey		keyData;		/* array of index qualifier descriptors */
+	int			numberOfOrderBys;	/* number of ordering operators */
+	int			numberOfNonNullOrderBys;	/* number of ordering operators
+											 * with non-NULL arguments */
+	ScanKey		orderByData;	/* array of ordering op descriptors */
+	Oid		   *orderByTypes;	/* array of ordering op return types */
+	int		   *nonNullOrderByOffsets;	/* array of offset of non-NULL
+										 * ordering keys in the original array */
+	Oid			indexCollation; /* collation of index column */
+
+	/* Opclass defined functions: */
+	FmgrInfo	innerConsistentFn;
+	FmgrInfo	leafConsistentFn;
+
+	/* Pre-allocated workspace arrays: */
+	double	   *zeroDistances;
+	double	   *infDistances;
+
+	/* These fields are only used in amgetbitmap scans: */
+	TIDBitmap  *tbm;			/* bitmap being filled */
+	int64		ntids;			/* number of TIDs passed to bitmap */
+
+	/* These fields are only used in amgettuple scans: */
+	bool		want_itup;		/* are we reconstructing tuples? */
+	TupleDesc	indexTupDesc;	/* if so, tuple descriptor for them */
+	int			nPtrs;			/* number of TIDs found on current page */
+	int			iPtr;			/* index for scanning through same */
+	ItemPointerData heapPtrs[MaxIndexTuplesPerPage];	/* TIDs from cur page */
+	bool		recheck[MaxIndexTuplesPerPage]; /* their recheck flags */
+	bool		recheckDistances[MaxIndexTuplesPerPage];	/* distance recheck
+															 * flags */
+	HeapTuple	reconTups[MaxIndexTuplesPerPage];	/* reconstructed tuples */
+
+	/* distances (for recheck) */
+	IndexOrderByDistance *distances[MaxIndexTuplesPerPage];
+
+	/*
+	 * Note: using MaxIndexTuplesPerPage above is a bit hokey since
+	 * SpGistLeafTuples aren't exactly IndexTuples; however, they are larger,
+	 * so this is safe.
+	 */
+} SpGistScanOpaqueData;
+
+typedef SpGistScanOpaqueData *SpGistScanOpaque;
+
+/*
+ * This struct is what we actually keep in index->rd_amcache.  It includes
+ * static configuration information as well as the lastUsedPages cache.
+ */
+typedef struct SpGistCache
+{
+	spgConfigOut config;		/* filled in by opclass config method */
+
+	SpGistTypeDesc attType;		/* type of values to be indexed/restored */
+	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
+	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
+	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
+
+	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+} SpGistCache;
+
 /*
  * XLOG stuff
  */
@@ -456,9 +496,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int spgLeafTupleSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +507,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void spgDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +527,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..86510687c7 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -349,14 +349,13 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 
 DROP TABLE tbl;
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..213cce5c7c
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,143 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..44b340053b 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -182,7 +182,7 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 DROP TABLE tbl;
 
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..38ace74d4e
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,84 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
-- 
2.28.0

v1-0002-Add-VACUUM-ANALYZE-to-index-including-test.patchapplication/octet-stream; name=v1-0002-Add-VACUUM-ANALYZE-to-index-including-test.patchDownload
From eb0ed1054b766bd110b0d1675a93065c0185a60a Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 27 Aug 2020 19:55:37 +0400
Subject: [PATCH v1] Add VACUUM ANALYZE to index including test

---
 src/test/regress/expected/index_including.out | 1 +
 src/test/regress/sql/index_including.sql      | 1 +
 2 files changed, 2 insertions(+)

diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..6a2a13ffa2 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -146,6 +146,7 @@ select * from tbl where (c1,c2,c3) < (2,5,1);
 
 -- row comparison that compares high key at page boundary
 SET enable_seqscan = off;
+VACUUM ANALYZE tbl;
 explain (costs off)
 select * from tbl where (c1,c2,c3) < (262,1,1) limit 1;
                      QUERY PLAN                     
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..1f300fe3b6 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -78,6 +78,7 @@ select * from tbl where (c1,c2,c3) < (2,5,1);
 select * from tbl where (c1,c2,c3) < (2,5,1);
 -- row comparison that compares high key at page boundary
 SET enable_seqscan = off;
+VACUUM ANALYZE tbl;
 explain (costs off)
 select * from tbl where (c1,c2,c3) < (262,1,1) limit 1;
 select * from tbl where (c1,c2,c3) < (262,1,1) limit 1;
-- 
2.28.0

#13Andrey M. Borodin
x4mmm@yandex-team.ru
In reply to: Pavel Borisov (#12)
Re: [PATCH] Covering SPGiST index

27 авг. 2020 г., в 21:03, Pavel Borisov <pashkin.elfe@gmail.com> написал(а):

see v8

For me is the only concerning point is putting nullmask and varatt bits into tuple->nextOffset.
But, probably, we can go with this.

But let's change macro a bit. When I see
SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
I expect that leafTuple->nextOffset is function argument by value and will not be changed.
For example see ItemPointerSetOffsetNumber() - it's not exposing ip_posid.

Also, I'd propose instead of

*(leafChainDatums + i * natts) and leafChainIsnulls + i * natts

using something like

int some_index = i * natts;
leafChainDatumsp[some_index] and &leafChainIsnulls[some_index]

But, probably, it's a matter of taste...

Also I'm not sure would it be helpful to use instead of

isnull[0] and leafDatum[0]

more complex

#define SpgKeyIndex 0
isnull[SpgKeyIndex] and leafDatum[SpgKeyIndex]

There is so many [0] in the patch...

Thanks!

Best regards, Andrey Borodin.

#14Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Andrey M. Borodin (#13)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

But let's change macro a bit. When I see
SGLT_SET_OFFSET(leafTuple->nextOffset, InvalidOffsetNumber);
I expect that leafTuple->nextOffset is function argument by value and will
not be changed.
For example see ItemPointerSetOffsetNumber() - it's not exposing ip_posid.

Also, I'd propose instead of

*(leafChainDatums + i * natts) and leafChainIsnulls + i * natts

using something like

int some_index = i * natts;
leafChainDatumsp[some_index] and &leafChainIsnulls[some_index]

But, probably, it's a matter of taste...

Also I'm not sure would it be helpful to use instead of

isnull[0] and leafDatum[0]

more complex

#define SpgKeyIndex 0
isnull[SpgKeyIndex] and leafDatum[SpgKeyIndex]

There is so many [0] in the patch...

I agree with all of your proposals and integrated them into v9.
Thank you very much!

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v9-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchapplication/octet-stream; name=v9-0001-Covering-SP-GiST-index-support-for-INCLUDE-column.patchDownload
From 717f09b9a7b94af1111a6812a13c124f1a600bf4 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Mon, 31 Aug 2020 15:38:11 +0400
Subject: [PATCH v9] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE columns for SPGiST index is intended to increase the speed of queries by making scans index-only likewise
in btree and GiST index. These columns are added only to leaf tuples and they are not used in index tree search but they
can be fetched during index scan.

The other point of INCLUDE columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in certain
cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster, update etc. Also, any data types without SP-GiST supported opclasses can be included.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/spgist/README              |  21 +-
 src/backend/access/spgist/spgdoinsert.c       | 175 +++++---
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  87 +++-
 src/backend/access/spgist/spgutils.c          | 389 ++++++++++++++++--
 src/backend/access/spgist/spgvacuum.c         |  25 +-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/spgist_private.h           | 273 +++++++-----
 src/test/regress/expected/amutils.out         |   4 +-
 src/test/regress/expected/index_including.out |   3 +-
 .../expected/index_including_spgist.out       | 143 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 src/test/regress/sql/index_including.sql      |   2 +-
 .../regress/sql/index_including_spgist.sql    |  84 ++++
 18 files changed, 997 insertions(+), 239 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 28adaba72d..c89cc6cb08 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index ff87b2d28f..3d360bcf47 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 0e04a08679..868a140a6a 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -240,6 +240,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..55b515f03d 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -73,9 +73,22 @@ Leaf tuple consists of:
     Example:
         radix tree - the rest of string (postfix)
         quad and k-d tree - the point itself
-
   ItemPointer to the heap
-
+  nextOffset number of next leaf tuple in a chain on a leaf page
+  optional nullmask for INCLUDE columns
+  optional INCLUDE columns values
+
+Leaf tuple layout changed since PostgreSQL version 14 to support INCLUDE
+columns but in a way that doesn't change the header and the key value
+placement in a tuple. So indexes created earlier remain fully supported.
+
+Also it is intended to be laid out with minimum possible gaps to make index
+smaller. I.e. first header of 12 bytes, then a key value starting from
+maxalign boundary, then just immediately nulls mask bytes, then INCLUDE
+attributes each starting from its typealign boundary. So in many cases,
+nullmask is stored free of charge and tuple occupy minimum possible space
+(with exception of gap before key value which starts from maxalign for
+compatibility).
 
 NULLS HANDLING
 
@@ -90,6 +103,10 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For INCLUDE attributes nulls are handled in ordinary per leaf-tuple way i.e.
+if null mask presence bit in a header is set, nullmask is added just after
+key value before the first INCLUDE attribute. Note that nullmask presence
+bit and nullmask itself apply only to INCLUDE attributes.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..335bbdb9dc 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple, SGLT_GET_OFFSET(head));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,11 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
+	int			chainStoreIndex; /* Index for start of datums/isnulls for a
+									current chain item */
 
 	in.level = level;
 
@@ -723,14 +728,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +745,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in the chain are
+	 * collected into 2-d arrays: (number of tuples in the chain) x (number of
+	 * attributes) The first attribute is key, the other - INCLUDE attributes (if
+	 * any). After picksplit we need to form new leaf tuples as the key attribute
+	 * length can change which can affect the alignment of every INCLUDE
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +774,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state, &leafChainDatums[chainStoreIndex],
+									  &leafChainIsnulls[chainStoreIndex], isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +802,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state, &leafChainDatums[chainStoreIndex],
+									  &leafChainIsnulls[chainStoreIndex], isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +816,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +824,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +837,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+	chainStoreIndex = in.nTuples * natts;
+	spgDeformLeafTuple(newLeafTuple, state, &leafChainDatums[chainStoreIndex],
+						  &leafChainIsnulls[chainStoreIndex], isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +865,13 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) out.leafTupleDatums[i];
+			leafChainIsnulls[chainStoreIndex] = false;
+
 			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +892,16 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) 0;
+			leafChainIsnulls[chainStoreIndex] = true;
+
 			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1235,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1928,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum. Otherwise, store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[spgKeyColumn] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[spgKeyColumn]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[spgKeyColumn] = PointerGetDatum(PG_DETOAST_DATUM(datum[spgKeyColumn]));
 			else
-				leafDatum = datum;
+				leafDatum[spgKeyColumn] = datum[spgKeyColumn];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[spgKeyColumn] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = spgLeafTupleSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2016,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[spgKeyColumn] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2050,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[spgKeyColumn] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2092,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[spgKeyColumn] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2109,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[spgKeyColumn], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2123,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[spgKeyColumn]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[spgKeyColumn], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2165,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[spgKeyColumn];
+			in.leafDatum = leafDatum[spgKeyColumn];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2176,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[spgKeyColumn])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2213,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[spgKeyColumn])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[spgKeyColumn] = out.result.matchNode.restDatum;
+						leafSize = spgLeafTupleSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2282,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..aff130f78a 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are INCLUDE attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add INCLUDE attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			spgDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[spgKeyColumn] = leafValue;
+			leafIsnulls[spgKeyColumn] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* INCLUDE attributes can always be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..bffe945843 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -116,14 +127,21 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have INCLUDE
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +174,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -177,7 +196,23 @@ spgGetCache(Relation index)
 		/* assume it's up to date */
 		cache = (SpGistCache *) index->rd_amcache;
 	}
+	/* Form descriptor for INCLUDE columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
 
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
 	return cache;
 }
 
@@ -190,6 +225,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,8 +639,8 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
- * Also, we follow the SPGiST convention that pass-by-val types are
+ * Note the result is not maxaligned and this should be done by the caller if
+ * needed. Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
 unsigned int
@@ -619,7 +655,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +678,205 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * at MAXALIGN boundary. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of INCLUDE
+ * data area in SpGiST leaf tuple. MAXALINGing first INCLUDE attribute is
+ * avoided for not to introduce unnecessary gap before it.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed for backward
+ * compatibility and there might be a gap between header and key data. After
+ * key data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed which is anyway unavoidable
+ * when placing a tuple on a page.
+ */
+unsigned int
+spgLeafTupleSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for INCLUDE attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[spgKeyColumn])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[spgKeyColumn]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall INCLUDE attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and INCLUDE
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to INCLUDE attributes and is placed just after key data if
+ * there is at least one NULL among INCLUDE attributes. It doesn't need alignment.
+ * Then all INCLUDE columns data follow aligned by their typealign-s.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are INCLUDE attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[spgKeyColumn])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[spgKeyColumn]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all INCLUDE attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
-	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * Ensure that we can replace the tuple with a dead tuple later. This
+	 * test is unnecessary when !isnull[spgKeyColumn], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[spgKeyColumn])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[spgKeyColumn]);
+
+	/* Add INCLUDE columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of INCLUDE attributes tuple (include_offset) is next
+		 * byte after end of a key value and is not required to be aligned.
+		 * Nullmask is included without alignment and values alignment are
+		 * done by heap_fill_tuple() automatically.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup, true);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup, true);
+	}
 	return tup;
 }
 
@@ -688,10 +893,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +940,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -1046,3 +1251,133 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+spgDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of INCLUDE data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[spgKeyColumn] = (Datum) 0;
+		isnull[spgKeyColumn] = true;
+	}
+	else
+	{
+		datum[spgKeyColumn] = SGLTDATUM(tup, state);
+		isnull[spgKeyColumn] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[spgKeyColumn]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..f23f9d0b1e 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt) > max ||
+					predecessor[SGLT_GET_OFFSET(lt)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits have their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..bbc2b91abc 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head) == SGLT_GET_OFFSET(&leafTupleHdr));
+				SGLT_SET_OFFSET(head, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..84ef6095ea 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,13 +22,14 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 } SpGistOptions;
 
+#define spgKeyColumn 0
+
 #define SpGistGetFillFactor(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == SPGIST_AM_OID), \
@@ -141,6 +142,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of INCLUDE columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,104 +150,6 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
-/*
- * Private state of an index scan
- */
-typedef struct SpGistScanOpaqueData
-{
-	SpGistState state;			/* see above */
-	pairingheap *scanQueue;		/* queue of to be visited items */
-	MemoryContext tempCxt;		/* short-lived memory context */
-	MemoryContext traversalCxt; /* single scan lifetime memory context */
-
-	/* Control flags showing whether to search nulls and/or non-nulls */
-	bool		searchNulls;	/* scan matches (all) null entries */
-	bool		searchNonNulls; /* scan matches (some) non-null entries */
-
-	/* Index quals to be passed to opclass (null-related quals removed) */
-	int			numberOfKeys;	/* number of index qualifier conditions */
-	ScanKey		keyData;		/* array of index qualifier descriptors */
-	int			numberOfOrderBys;	/* number of ordering operators */
-	int			numberOfNonNullOrderBys;	/* number of ordering operators
-											 * with non-NULL arguments */
-	ScanKey		orderByData;	/* array of ordering op descriptors */
-	Oid		   *orderByTypes;	/* array of ordering op return types */
-	int		   *nonNullOrderByOffsets;	/* array of offset of non-NULL
-										 * ordering keys in the original array */
-	Oid			indexCollation; /* collation of index column */
-
-	/* Opclass defined functions: */
-	FmgrInfo	innerConsistentFn;
-	FmgrInfo	leafConsistentFn;
-
-	/* Pre-allocated workspace arrays: */
-	double	   *zeroDistances;
-	double	   *infDistances;
-
-	/* These fields are only used in amgetbitmap scans: */
-	TIDBitmap  *tbm;			/* bitmap being filled */
-	int64		ntids;			/* number of TIDs passed to bitmap */
-
-	/* These fields are only used in amgettuple scans: */
-	bool		want_itup;		/* are we reconstructing tuples? */
-	TupleDesc	indexTupDesc;	/* if so, tuple descriptor for them */
-	int			nPtrs;			/* number of TIDs found on current page */
-	int			iPtr;			/* index for scanning through same */
-	ItemPointerData heapPtrs[MaxIndexTuplesPerPage];	/* TIDs from cur page */
-	bool		recheck[MaxIndexTuplesPerPage]; /* their recheck flags */
-	bool		recheckDistances[MaxIndexTuplesPerPage];	/* distance recheck
-															 * flags */
-	HeapTuple	reconTups[MaxIndexTuplesPerPage];	/* reconstructed tuples */
-
-	/* distances (for recheck) */
-	IndexOrderByDistance *distances[MaxIndexTuplesPerPage];
-
-	/*
-	 * Note: using MaxIndexTuplesPerPage above is a bit hokey since
-	 * SpGistLeafTuples aren't exactly IndexTuples; however, they are larger,
-	 * so this is safe.
-	 */
-} SpGistScanOpaqueData;
-
-typedef SpGistScanOpaqueData *SpGistScanOpaque;
-
-/*
- * This struct is what we actually keep in index->rd_amcache.  It includes
- * static configuration information as well as the lastUsedPages cache.
- */
-typedef struct SpGistCache
-{
-	spgConfigOut config;		/* filled in by opclass config method */
-
-	SpGistTypeDesc attType;		/* type of values to be indexed/restored */
-	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
-	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
-	SpGistTypeDesc attLabelType;	/* type of node label values */
-
-	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
-} SpGistCache;
-
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +209,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,11 +226,13 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a key datum, a heap tuple TID and optional
+ * datums and nullmask of INCLUDE columns.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of INCLUDE columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
@@ -346,14 +252,43 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Minimum space to store SpGistLeafTuple plus ItemIdData on a page is 16 bytes,
+ * so 14 lower bits of nextOffset is enough to store tuple number in a chain
+ * on a page even if a page size is 64Kb. Two higher bits are to store per-tuple
+ * information for INCLUDE attributes: is there nulls mask exist, and are there
+ * any INCLUDE attributes of variable length type. If there are no INCLUDE
+ * columns these higher bits are not used.
+ *
+ * If there are INCLUDE columns, they are stored after a key value, each
+ * starting from its own typalign boundary. Unlike IndexTuple, first INCLUDE
+ * value does not need to start from MAXALIGN boundary, so SPGiST uses private
+ * routines to access them. Nullmask with size (number of INCLUDE columns)/8
+ * bytes is put without alignment between the key and the first INCLUDE column.
+ * If there is an alignment gap between them, nullmask has a good chance to fit
+ * into the gap, thus making its storage free of charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * nextOffset is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: INCLUDE values has nulls
+	 * 14th bit: INCLUDE values has var-length attributes
+	 * 13-0 bit: number of next tuple in chain on a page, or InvalidOffsetNumber
+	 * ---------------
+	 */
+
+	unsigned short nextOffset;	/* info for linking tuples in a chain on a leaf page,
+								   and additional info for INCLUDE attributes */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* key column data follows */
+	/* nullmask of INCLUDE values follows if there are nulls in INCLUDE attributes*/
+	/* INCLUDE columns data follow if any */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
@@ -361,8 +296,25 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
 #define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
 #define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
 #define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Macros to access bit fields inside nextOffset independently.
+ */
+#define SGLT_GET_OFFSET(spgLeafTuple)	( (spgLeafTuple)->nextOffset & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(spgLeafTuple) \
+	( (bool)((spgLeafTuple)->nextOffset >> 15) )
+#define SGLT_GET_CONTAINSVARATT(spgLeafTuple) \
+	( (bool)(((spgLeafTuple)->nextOffset & 0x4000) >> 14) )
+#define SGLT_SET_OFFSET(spgLeafTuple, offsetNumber) \
+	( (spgLeafTuple)->nextOffset = \
+	((spgLeafTuple)->nextOffset & 0xC000) | ((offsetNumber) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(spgLeafTuple, is_null) \
+	( (spgLeafTuple)->nextOffset = \
+	((uint16)(bool)(is_null) << 15) | ((spgLeafTuple)->nextOffset & 0x3FFF) )
+#define SGLT_SET_CONTAINSVARATT(spgLeafTuple, is_varatt) \
+	( (spgLeafTuple)->nextOffset = \
+	((uint16)(bool)(is_varatt) << 14) | ((spgLeafTuple)->nextOffset & 0xBFFF) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -394,7 +346,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +361,105 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+
+/*
+ * Private state of an index scan
+ */
+typedef struct SpGistScanOpaqueData
+{
+	SpGistState state;			/* see above */
+	pairingheap *scanQueue;		/* queue of to be visited items */
+	MemoryContext tempCxt;		/* short-lived memory context */
+	MemoryContext traversalCxt; /* single scan lifetime memory context */
+
+	/* Control flags showing whether to search nulls and/or non-nulls */
+	bool		searchNulls;	/* scan matches (all) null entries */
+	bool		searchNonNulls; /* scan matches (some) non-null entries */
+
+	/* Index quals to be passed to opclass (null-related quals removed) */
+	int			numberOfKeys;	/* number of index qualifier conditions */
+	ScanKey		keyData;		/* array of index qualifier descriptors */
+	int			numberOfOrderBys;	/* number of ordering operators */
+	int			numberOfNonNullOrderBys;	/* number of ordering operators
+											 * with non-NULL arguments */
+	ScanKey		orderByData;	/* array of ordering op descriptors */
+	Oid		   *orderByTypes;	/* array of ordering op return types */
+	int		   *nonNullOrderByOffsets;	/* array of offset of non-NULL
+										 * ordering keys in the original array */
+	Oid			indexCollation; /* collation of index column */
+
+	/* Opclass defined functions: */
+	FmgrInfo	innerConsistentFn;
+	FmgrInfo	leafConsistentFn;
+
+	/* Pre-allocated workspace arrays: */
+	double	   *zeroDistances;
+	double	   *infDistances;
+
+	/* These fields are only used in amgetbitmap scans: */
+	TIDBitmap  *tbm;			/* bitmap being filled */
+	int64		ntids;			/* number of TIDs passed to bitmap */
+
+	/* These fields are only used in amgettuple scans: */
+	bool		want_itup;		/* are we reconstructing tuples? */
+	TupleDesc	indexTupDesc;	/* if so, tuple descriptor for them */
+	int			nPtrs;			/* number of TIDs found on current page */
+	int			iPtr;			/* index for scanning through same */
+	ItemPointerData heapPtrs[MaxIndexTuplesPerPage];	/* TIDs from cur page */
+	bool		recheck[MaxIndexTuplesPerPage]; /* their recheck flags */
+	bool		recheckDistances[MaxIndexTuplesPerPage];	/* distance recheck
+															 * flags */
+	HeapTuple	reconTups[MaxIndexTuplesPerPage];	/* reconstructed tuples */
+
+	/* distances (for recheck) */
+	IndexOrderByDistance *distances[MaxIndexTuplesPerPage];
+
+	/*
+	 * Note: using MaxIndexTuplesPerPage above is a bit hokey since
+	 * SpGistLeafTuples aren't exactly IndexTuples; however, they are larger,
+	 * so this is safe.
+	 */
+} SpGistScanOpaqueData;
+
+typedef SpGistScanOpaqueData *SpGistScanOpaque;
+
+/*
+ * This struct is what we actually keep in index->rd_amcache.  It includes
+ * static configuration information as well as the lastUsedPages cache.
+ */
+typedef struct SpGistCache
+{
+	spgConfigOut config;		/* filled in by opclass config method */
+
+	SpGistTypeDesc attType;		/* type of values to be indexed/restored */
+	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
+	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
+	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
+
+	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+} SpGistCache;
+
 /*
  * XLOG stuff
  */
@@ -456,9 +506,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int spgLeafTupleSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +517,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void spgDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +537,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..86510687c7 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -349,14 +349,13 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 
 DROP TABLE tbl;
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..213cce5c7c
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,143 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..44b340053b 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -182,7 +182,7 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 DROP TABLE tbl;
 
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..38ace74d4e
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,84 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
-- 
2.28.0

#15Andrey M. Borodin
x4mmm@yandex-team.ru
In reply to: Pavel Borisov (#14)
Re: [PATCH] Covering SPGiST index

31 авг. 2020 г., в 16:57, Pavel Borisov <pashkin.elfe@gmail.com> написал(а):

I agree with all of your proposals and integrated them into v9.

I have a wild idea of renaming nextOffset in SpGistLeafTupleData.
Or at least documenting in comments that this field is more than just an offset.

This looks like assert rather than real runtime check in spgLeafTupleSize()

+		if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							state->includeTupdesc->natts, INDEX_MAX_KEYS)));
Even if you go with check, number of columns is state->includeTupdesc->natts  + 1.
Also I'd refactor this
+	/* Form descriptor for INCLUDE columns if any */
+	if (IndexRelationGetNumberOfAttributes(index) > 1)
+	{
+		int			i;
+
+		cache->includeTupdesc = CreateTemplateTupleDesc(
+														IndexRelationGetNumberOfAttributes(index) - 1);
+		for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+		{
+			TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i + 1)->atttypid,
+							   -1, 0);
+		}
+	}
+	else
+		cache->includeTupdesc = NULL;
into something like
cache->includeTupdesc = NULL;
for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
{
    if (cache->includeTupdesc == NULL)
	// init tuple description
    // init entry
}
But, probably it's only a matter of taste.

Beside this, I think patch is ready for committer. If Anastasia has no objections, let's flip CF entry state.

Thanks!

Best regards, Andrey Borodin.

#16Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Andrey M. Borodin (#15)
2 attachment(s)
Re: [PATCH] Covering SPGiST index

I have a wild idea of renaming nextOffset in SpGistLeafTupleData.
Or at least documenting in comments that this field is more than just an
offset.

Seems reasonable as previous changes localized mentions of nextOffset only
to leaf tuple definition and access macros. So I've done this renaming.

This looks like assert rather than real runtime check in spgLeafTupleSize()

+               if (state->includeTupdesc->natts + 1 >= INDEX_MAX_KEYS)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_TOO_MANY_COLUMNS),
+                                        errmsg("number of index columns
(%d) exceeds limit (%d)",
+
state->includeTupdesc->natts, INDEX_MAX_KEYS)));
Even if you go with check, number of columns is
state->includeTupdesc->natts  + 1.

I placed this check only once on SpGist state creation and replaced the
other checks to asserts.

Also I'd refactor this
+ /* Form descriptor for INCLUDE columns if any */

Also done. Thanks a lot!
See v10.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v10-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchapplication/octet-stream; name=v10-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchDownload
From ea7f789574612d27007980022917cc7532899c53 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Wed, 2 Sep 2020 19:38:49 +0400
Subject: [PATCH v10] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE columns for SPGiST index is intended to increase the speed of queries by making scans index-only likewise
in btree and GiST index. These columns are added only to leaf tuples and they are not used in index tree search but they
can be fetched during index scan.

The other point of INCLUDE columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in certain
cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster, update etc. Also, any data types without SP-GiST supported opclasses can be included.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/spgist/README              |  21 +-
 src/backend/access/spgist/spgdoinsert.c       | 175 +++++---
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  87 +++-
 src/backend/access/spgist/spgutils.c          | 381 ++++++++++++++++--
 src/backend/access/spgist/spgvacuum.c         |  25 +-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/spgist_private.h           | 286 +++++++------
 src/test/regress/expected/amutils.out         |   4 +-
 src/test/regress/expected/index_including.out |   3 +-
 .../expected/index_including_spgist.out       | 143 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 src/test/regress/sql/index_including.sql      |   2 +-
 .../regress/sql/index_including_spgist.sql    |  84 ++++
 18 files changed, 995 insertions(+), 246 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 28adaba72d..c89cc6cb08 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index ff87b2d28f..3d360bcf47 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 0e04a08679..868a140a6a 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -240,6 +240,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..55b515f03d 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -73,9 +73,22 @@ Leaf tuple consists of:
     Example:
         radix tree - the rest of string (postfix)
         quad and k-d tree - the point itself
-
   ItemPointer to the heap
-
+  nextOffset number of next leaf tuple in a chain on a leaf page
+  optional nullmask for INCLUDE columns
+  optional INCLUDE columns values
+
+Leaf tuple layout changed since PostgreSQL version 14 to support INCLUDE
+columns but in a way that doesn't change the header and the key value
+placement in a tuple. So indexes created earlier remain fully supported.
+
+Also it is intended to be laid out with minimum possible gaps to make index
+smaller. I.e. first header of 12 bytes, then a key value starting from
+maxalign boundary, then just immediately nulls mask bytes, then INCLUDE
+attributes each starting from its typealign boundary. So in many cases,
+nullmask is stored free of charge and tuple occupy minimum possible space
+(with exception of gap before key value which starts from maxalign for
+compatibility).
 
 NULLS HANDLING
 
@@ -90,6 +103,10 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For INCLUDE attributes nulls are handled in ordinary per leaf-tuple way i.e.
+if null mask presence bit in a header is set, nullmask is added just after
+key value before the first INCLUDE attribute. Note that nullmask presence
+bit and nullmask itself apply only to INCLUDE attributes.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..335bbdb9dc 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple, SGLT_GET_OFFSET(head));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,11 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
+	int			chainStoreIndex; /* Index for start of datums/isnulls for a
+									current chain item */
 
 	in.level = level;
 
@@ -723,14 +728,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +745,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in the chain are
+	 * collected into 2-d arrays: (number of tuples in the chain) x (number of
+	 * attributes) The first attribute is key, the other - INCLUDE attributes (if
+	 * any). After picksplit we need to form new leaf tuples as the key attribute
+	 * length can change which can affect the alignment of every INCLUDE
+	 * attribute.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +774,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state, &leafChainDatums[chainStoreIndex],
+									  &leafChainIsnulls[chainStoreIndex], isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +802,9 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state, &leafChainDatums[chainStoreIndex],
+									  &leafChainIsnulls[chainStoreIndex], isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +816,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +824,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +837,17 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+	chainStoreIndex = in.nTuples * natts;
+	spgDeformLeafTuple(newLeafTuple, state, &leafChainDatums[chainStoreIndex],
+						  &leafChainIsnulls[chainStoreIndex], isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +865,13 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) out.leafTupleDatums[i];
+			leafChainIsnulls[chainStoreIndex] = false;
+
 			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +892,16 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) 0;
+			leafChainIsnulls[chainStoreIndex] = true;
+
 			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1235,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1928,83 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum. Otherwise, store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[spgKeyColumn] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[spgKeyColumn]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[spgKeyColumn] = PointerGetDatum(PG_DETOAST_DATUM(datum[spgKeyColumn]));
 			else
-				leafDatum = datum;
+				leafDatum[spgKeyColumn] = datum[spgKeyColumn];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[spgKeyColumn] = (Datum) 0;
+
+	for (i = 1; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->includeTupdesc, i - 1)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = spgLeafTupleSize(state, leafDatum, isnull) + sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2016,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[spgKeyColumn] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2050,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[spgKeyColumn] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2092,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[spgKeyColumn] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2109,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[spgKeyColumn], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2123,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[spgKeyColumn]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[spgKeyColumn], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2165,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[spgKeyColumn];
+			in.leafDatum = leafDatum[spgKeyColumn];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2176,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[spgKeyColumn])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2213,11 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[spgKeyColumn])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[spgKeyColumn] = out.result.matchNode.restDatum;
+						leafSize = spgLeafTupleSize(state, leafDatum, isnull) +
+							sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2282,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..aff130f78a 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,30 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are INCLUDE attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.includeTupdesc)
+	{
+		Assert(so->state.includeTupdesc->natts);
+
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +525,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +552,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +567,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +762,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +776,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple);
 }
 
 /*
@@ -782,8 +808,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +903,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +930,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +975,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.includeTupdesc)
+		{
+			/* Add INCLUDE attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			Assert(so->state.includeTupdesc->natts);
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.includeTupdesc->natts + 1));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.includeTupdesc->natts + 1));
+
+			spgDeformLeafTuple(leafTuple, &so->state, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[spgKeyColumn] = leafValue;
+			leafIsnulls[spgKeyColumn] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1074,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* INCLUDE attributes can always be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 0efe05e552..cbe4012074 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,18 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
+/* Does att's datatype allow packing into the 1-byte-header varlena format? */
+#define ATT_IS_PACKABLE(att) \
+		((att)->attlen == -1 && (att)->attstorage != TYPSTORAGE_PLAIN)
+
+Size		spgIncludedDataSize(TupleDesc tupleDesc, Datum *values,
+								bool *isnull, Size start);
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -49,7 +60,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amcanorderbyop = true;
 	amroutine->amcanbackward = false;
 	amroutine->amcanunique = false;
-	amroutine->amcanmulticol = false;
+	amroutine->amcanmulticol = true;
 	amroutine->amoptionalkey = true;
 	amroutine->amsearcharray = false;
 	amroutine->amsearchnulls = true;
@@ -57,7 +68,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -104,6 +115,7 @@ SpGistCache *
 spgGetCache(Relation index)
 {
 	SpGistCache *cache;
+	int i;
 
 	if (index->rd_amcache == NULL)
 	{
@@ -116,14 +128,26 @@ spgGetCache(Relation index)
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
 									   sizeof(SpGistCache));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/*
+		 * SPGiST should have one key column and can also have INCLUDE
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
+		if (IndexRelationGetNumberOfAttributes(index) >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					errmsg("number of index columns (%d) exceeds limit (%d)",
+					IndexRelationGetNumberOfAttributes(index), INDEX_MAX_KEYS)));
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -156,6 +180,7 @@ spgGetCache(Relation index)
 		fillTypeDesc(&cache->attPrefixType, cache->config.prefixType);
 		fillTypeDesc(&cache->attLabelType, cache->config.labelType);
 
+
 		/* Last, get the lastUsedPages data from the metapage */
 		metabuffer = ReadBuffer(index, SPGIST_METAPAGE_BLKNO);
 		LockBuffer(metabuffer, BUFFER_LOCK_SHARE);
@@ -178,6 +203,18 @@ spgGetCache(Relation index)
 		cache = (SpGistCache *) index->rd_amcache;
 	}
 
+	/* Form descriptor for INCLUDE columns if any */
+	cache->includeTupdesc = NULL;
+	for (i = 0; i < IndexRelationGetNumberOfAttributes(index) - 1; i++)
+	{
+		if (cache->includeTupdesc == NULL)
+			cache->includeTupdesc = CreateTemplateTupleDesc(
+				IndexRelationGetNumberOfAttributes(index) - 1);
+
+		TupleDescInitEntry(cache->includeTupdesc, i + 1, NULL,
+				TupleDescAttr(index->rd_att, i + 1)->atttypid, -1, 0);
+	}
+
 	return cache;
 }
 
@@ -190,6 +227,7 @@ initSpGistState(SpGistState *state, Relation index)
 	/* Get cached static information about index */
 	cache = spgGetCache(index);
 
+	state->includeTupdesc = cache->includeTupdesc;
 	state->config = cache->config;
 	state->attType = cache->attType;
 	state->attLeafType = cache->attLeafType;
@@ -603,8 +641,8 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
- * Also, we follow the SPGiST convention that pass-by-val types are
+ * Note the result is not maxaligned and this should be done by the caller if
+ * needed. Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
 unsigned int
@@ -619,7 +657,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +680,197 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 }
 
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Private version of heap_compute_data_size with start address not
+ * at MAXALIGN boundary. The reason is that start address (and alignment)
+ * influence alignment of each of next values and overall size of INCLUDE
+ * data area in SpGiST leaf tuple. MAXALINGing first INCLUDE attribute is
+ * avoided for not to introduce unnecessary gap before it.
+ */
+Size
+spgIncludedDataSize(TupleDesc tupleDesc,
+					Datum *values,
+					bool *isnull, Size start)
+{
+	Size		data_length = 0;
+	int			i;
+	int			numberOfAttributes = tupleDesc->natts;
+
+	data_length = start;
+	for (i = 0; i < numberOfAttributes; i++)
+	{
+		Datum		val;
+		Form_pg_attribute atti;
+
+		if (isnull[i])
+			continue;
+
+		val = values[i];
+		atti = TupleDescAttr(tupleDesc, i);
+
+		if (ATT_IS_PACKABLE(atti) &&
+			VARATT_CAN_MAKE_SHORT(DatumGetPointer(val)))
+		{
+			/*
+			 * we're anticipating converting to a short varlena header, so
+			 * adjust length and don't count any alignment
+			 */
+			data_length += VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(val));
+		}
+		else if (atti->attlen == -1 &&
+				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
+		{
+			/*
+			 * we want to flatten the expanded value so that the constructed
+			 * tuple doesn't depend on it
+			 */
+			data_length = att_align_nominal(data_length, atti->attalign);
+			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+		}
+		else
+		{
+			data_length = att_align_datum(data_length, atti->attalign,
+										  atti->attlen, val);
+			data_length = att_addlength_datum(data_length, atti->attlen,
+											  val);
+		}
+	}
+	return data_length - start;
+}
+
+/* Calculate overall leaf tuple size. SGLTHDRSZ is MAXALIGNed for backward
+ * compatibility and there might be a gap between header and key data. After
+ * key data there are no such gaps more than is is necessary for each value
+ * alignment. Overall result is MAXALIGNed which is anyway unavoidable
+ * when placing a tuple on a page.
+ */
+unsigned int
+spgLeafTupleSize(SpGistState *state, Datum *datum, bool *isnull)
+{
+	/* compute space needed, nullmask size and offset for INCLUDE attributes */
+	unsigned int size = SGLTHDRSZ;
+	unsigned int i;
+
+	if (!isnull[spgKeyColumn])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[spgKeyColumn]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		Assert(state->includeTupdesc->natts + 1 <= INDEX_MAX_KEYS);
+		/* nullmask size */
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				size += (state->includeTupdesc->natts / 8) + 1;
+				break;
+			}
+		}
+		/* overall INCLUDE attributes size each with added proper alignment. */
+		size += spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+	}
+	return MAXALIGN(size);
+}
+
+/*
+ * Construct a leaf tuple containing the given heap TID, key data and INCLUDE
+ * columns data. Key data starts from MAXALIGN boundary for backward compatibility.
+ * Nullmask apply only to INCLUDE attributes and is placed just after key data if
+ * there is at least one NULL among INCLUDE attributes. It doesn't need alignment.
+ * Then all INCLUDE columns data follow aligned by their typealign-s.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
+	unsigned int size = SGLTHDRSZ;
+	unsigned int include_offset = 0;
+	unsigned int nullmask_size = 0;
+	unsigned int data_offset = 0;
+	unsigned int data_size = 0;
+	uint16		tupmask = 0;
+	int			i;
 
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	/*
+	 * Calculate space needed. If there are INCLUDE attributes also calculate
+	 * sizes and offsets needed for heap_fill_tuple
+	 */
+	if (!isnull[spgKeyColumn])
+		/* key attribute size (not maxaligned) */
+		size += SpGistGetTypeSize(&state->attLeafType, datum[spgKeyColumn]);
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		Assert(state->includeTupdesc->natts + 1 <= INDEX_MAX_KEYS);
+
+		include_offset = size;
+
+		for (i = 1; i <= state->includeTupdesc->natts; i++)
+		{
+			if (isnull[i])
+			{
+				nullmask_size = (state->includeTupdesc->natts / 8) + 1;
+				size += nullmask_size;
+				break;
+			}
+		}
+
+		/*
+		 * Alignment of all INCLUDE attributes is counted inside data_size.
+		 * data_offset itself is not aligned.
+		 */
+		data_size = spgIncludedDataSize(state->includeTupdesc, datum + 1, isnull + 1, size);
+		data_offset = size;
+
+		size += data_size;
+	}
 
 	/*
-	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * Ensure that we can replace the tuple with a dead tuple later. This
+	 * test is unnecessary when !isnull[spgKeyColumn], but let's be safe.
 	 */
 	if (size < SGDTSIZE)
 		size = SGDTSIZE;
 
 	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tup = (SpGistLeafTuple) palloc0(MAXALIGN(size));
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	tup->size = MAXALIGN(size);
+	SGLT_SET_OFFSET(tup, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
 
+	if (!isnull[spgKeyColumn])
+		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum[spgKeyColumn]);
+
+	/* Add INCLUDE columns data to leaf tuple if any. */
+	if (state->includeTupdesc)
+	{
+		/*
+		 * The start of INCLUDE attributes tuple (include_offset) is next
+		 * byte after end of a key value and is not required to be aligned.
+		 * Nullmask is included without alignment and values alignment are
+		 * done by heap_fill_tuple() automatically.
+		 */
+		heap_fill_tuple(state->includeTupdesc, datum + 1, isnull + 1,
+						(char *) tup + data_offset,
+						data_size, &tupmask,
+						(nullmask_size ? (bits8 *) tup + include_offset : NULL));
+
+		if (nullmask_size)
+			SGLT_SET_CONTAINSNULLMASK(tup, true);
+
+		/*
+		 * We do this because heap_fill_tuple wants to initialize a "tupmask"
+		 * which is used for HeapTuples, but the only relevant info is the
+		 * "has variable attributes" field. We have already set the hasnull
+		 * bit above.
+		 */
+		if (tupmask & HEAP_HASVARWIDTH)
+			SGLT_SET_CONTAINSVARATT(tup, true);
+	}
 	return tup;
 }
 
@@ -688,10 +887,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -735,7 +934,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -814,7 +1013,7 @@ spgFormDeadTuple(SpGistState *state, int tupstate,
 
 	tuple->tupstate = tupstate;
 	tuple->size = SGDTSIZE;
-	tuple->nextOffset = InvalidOffsetNumber;
+	tuple->t_info = InvalidOffsetNumber;
 
 	if (tupstate == SPGIST_REDIRECT)
 	{
@@ -1046,3 +1245,129 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ *
+ */
+void
+spgDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state, Datum *datum, bool *isnull,
+					  bool key_isnull)
+{
+	unsigned int include_offset;	/* offset of INCLUDE data */
+	int			off;
+	bits8	   *nullmask_ptr = NULL;	/* ptr to null bitmap in tuple */
+	char	   *tp;
+	bool		slow = false;	/* can we use/set attcacheoff? */
+	int			i;
+
+	if (key_isnull)
+	{
+		datum[spgKeyColumn] = (Datum) 0;
+		isnull[spgKeyColumn] = true;
+	}
+	else
+	{
+		datum[spgKeyColumn] = SGLTDATUM(tup, state);
+		isnull[spgKeyColumn] = false;
+	}
+
+	if (state->includeTupdesc)
+	{
+		Assert(state->includeTupdesc->natts);
+		Assert(state->includeTupdesc->natts + 1 <= INDEX_MAX_KEYS);
+
+		include_offset = key_isnull ? SGLTHDRSZ : SGLTHDRSZ + SpGistGetTypeSize(&state->attLeafType, datum[spgKeyColumn]);
+
+		tp = (char *) tup;
+		off = include_offset;
+
+		if (SGLT_GET_CONTAINSNULLMASK(tup))
+		{
+			nullmask_ptr = (bits8 *) tp + include_offset;
+			off += (state->includeTupdesc->natts) / 8 + 1;
+		}
+
+		if (state->attLeafType.attlen > 0 && !SGLT_GET_CONTAINSVARATT(tup) &&
+			!SGLT_GET_CONTAINSNULLMASK(tup))
+			/* can use attcacheoff for all attributes */
+		{
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				isnull[i] = false;
+				if (thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else
+				{
+					off = att_align_nominal(off, thisatt->attalign);
+					thisatt->attcacheoff = off;
+				}
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+			}
+		}
+		else
+
+			/*
+			 * general case: can use cache until first null or varlen
+			 * attribute
+			 */
+		{
+			if (state->attLeafType.attlen <= 0)
+				slow = true;	/* can't use attcacheoff at all */
+
+			for (i = 1; i <= state->includeTupdesc->natts; i++)
+			{
+				Form_pg_attribute thisatt = TupleDescAttr(state->includeTupdesc, i - 1);
+
+				if (SGLT_GET_CONTAINSNULLMASK(tup))
+				{
+					if (att_isnull(i - 1, nullmask_ptr))
+					{
+						datum[i] = (Datum) 0;
+						isnull[i] = true;
+						slow = true;	/* can't use attcacheoff anymore */
+						continue;
+					}
+				}
+
+				isnull[i] = false;
+
+				if (!slow && thisatt->attcacheoff >= 0)
+					off = thisatt->attcacheoff;
+				else if (thisatt->attlen == -1)
+				{
+					/*
+					 * We can only cache the offset for a varlena attribute if
+					 * the offset is already suitably aligned, so that there
+					 * would be no pad bytes in any case: then the offset will
+					 * be valid for either an aligned or unaligned value.
+					 */
+					if (!slow && off == att_align_nominal(off, thisatt->attalign))
+						thisatt->attcacheoff = off;
+					else
+					{
+						off = att_align_pointer(off, thisatt->attalign, -1, tp + off);
+						slow = true;
+					}
+				}
+				else
+				{
+					/* not varlena, so safe to use att_align_nominal */
+					off = att_align_nominal(off, thisatt->attalign);
+
+					if (!slow)
+						thisatt->attcacheoff = off;
+				}
+
+				datum[i] = fetchatt(thisatt, tp + off);
+				off = att_addlength_pointer(off, thisatt->attlen, tp + off);
+
+				if (thisatt->attlen <= 0)
+					slow = true;	/* can't use attcacheoff anymore */
+			}
+		}
+	}
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index bd98707f3c..f23f9d0b1e 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt) > max ||
+					predecessor[SGLT_GET_OFFSET(lt)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits have their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 7be2291d07..bbc2b91abc 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head) == SGLT_GET_OFFSET(&leafTupleHdr));
+				SGLT_SET_OFFSET(head, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..03cbf826a7 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,13 +22,14 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 } SpGistOptions;
 
+#define spgKeyColumn 0
+
 #define SpGistGetFillFactor(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == SPGIST_AM_OID), \
@@ -141,6 +142,7 @@ typedef struct SpGistState
 	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
 	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
 	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc; /* tuple descriptor of INCLUDE columns */
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
@@ -148,104 +150,6 @@ typedef struct SpGistState
 	bool		isBuild;		/* true if doing index build */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
-/*
- * Private state of an index scan
- */
-typedef struct SpGistScanOpaqueData
-{
-	SpGistState state;			/* see above */
-	pairingheap *scanQueue;		/* queue of to be visited items */
-	MemoryContext tempCxt;		/* short-lived memory context */
-	MemoryContext traversalCxt; /* single scan lifetime memory context */
-
-	/* Control flags showing whether to search nulls and/or non-nulls */
-	bool		searchNulls;	/* scan matches (all) null entries */
-	bool		searchNonNulls; /* scan matches (some) non-null entries */
-
-	/* Index quals to be passed to opclass (null-related quals removed) */
-	int			numberOfKeys;	/* number of index qualifier conditions */
-	ScanKey		keyData;		/* array of index qualifier descriptors */
-	int			numberOfOrderBys;	/* number of ordering operators */
-	int			numberOfNonNullOrderBys;	/* number of ordering operators
-											 * with non-NULL arguments */
-	ScanKey		orderByData;	/* array of ordering op descriptors */
-	Oid		   *orderByTypes;	/* array of ordering op return types */
-	int		   *nonNullOrderByOffsets;	/* array of offset of non-NULL
-										 * ordering keys in the original array */
-	Oid			indexCollation; /* collation of index column */
-
-	/* Opclass defined functions: */
-	FmgrInfo	innerConsistentFn;
-	FmgrInfo	leafConsistentFn;
-
-	/* Pre-allocated workspace arrays: */
-	double	   *zeroDistances;
-	double	   *infDistances;
-
-	/* These fields are only used in amgetbitmap scans: */
-	TIDBitmap  *tbm;			/* bitmap being filled */
-	int64		ntids;			/* number of TIDs passed to bitmap */
-
-	/* These fields are only used in amgettuple scans: */
-	bool		want_itup;		/* are we reconstructing tuples? */
-	TupleDesc	indexTupDesc;	/* if so, tuple descriptor for them */
-	int			nPtrs;			/* number of TIDs found on current page */
-	int			iPtr;			/* index for scanning through same */
-	ItemPointerData heapPtrs[MaxIndexTuplesPerPage];	/* TIDs from cur page */
-	bool		recheck[MaxIndexTuplesPerPage]; /* their recheck flags */
-	bool		recheckDistances[MaxIndexTuplesPerPage];	/* distance recheck
-															 * flags */
-	HeapTuple	reconTups[MaxIndexTuplesPerPage];	/* reconstructed tuples */
-
-	/* distances (for recheck) */
-	IndexOrderByDistance *distances[MaxIndexTuplesPerPage];
-
-	/*
-	 * Note: using MaxIndexTuplesPerPage above is a bit hokey since
-	 * SpGistLeafTuples aren't exactly IndexTuples; however, they are larger,
-	 * so this is safe.
-	 */
-} SpGistScanOpaqueData;
-
-typedef SpGistScanOpaqueData *SpGistScanOpaque;
-
-/*
- * This struct is what we actually keep in index->rd_amcache.  It includes
- * static configuration information as well as the lastUsedPages cache.
- */
-typedef struct SpGistCache
-{
-	spgConfigOut config;		/* filled in by opclass config method */
-
-	SpGistTypeDesc attType;		/* type of values to be indexed/restored */
-	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
-	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
-	SpGistTypeDesc attLabelType;	/* type of node label values */
-
-	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
-} SpGistCache;
-
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +209,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,21 +226,19 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a key datum, a heap tuple TID and optional
+ * datums and nullmask of INCLUDE columns.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of INCLUDE columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
  * wide to be stored immediately, and it costs nothing because of alignment
  * considerations.
  *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
  * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
  * so that the tuple can be converted to REDIRECT status later.  (This
  * restriction only adds bytes for the null-datum case, otherwise alignment
@@ -346,14 +248,48 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Normally, nextOffset inside t_info links to the next tuple belonging to
+ * the same parent node (which must be on the same page).  But when the root
+ * page is a leaf page, we don't chain its tuples, so nextOffset is always 0
+ * on the root. Minimum space to store SpGistLeafTuple plus ItemIdData on a
+ * page is 16 bytes, so 14 lower bits for nextOffset is enough to store tuple
+ * number in a chain on a page even if a page size is 64Kb.
+ *
+ * Two higher bits in t_info are to store per-tuple information for INCLUDE
+ * attributes: is there nulls mask exist, and are there any INCLUDE attributes
+ * of variable length type. If there are no INCLUDE columns these higher bits
+ * are not used and can have any values.
+ *
+ * If there are INCLUDE columns, they are stored after a key value, each
+ * starting from its own typalign boundary. Unlike IndexTuple, first INCLUDE
+ * value does not need to start from MAXALIGN boundary, so SPGiST uses private
+ * routines to access them. Nullmask with size (number of INCLUDE columns)/8
+ * bytes is put without alignment between the key and the first INCLUDE column.
+ * If there is an alignment gap between them, nullmask has a good chance to fit
+ * into the gap, thus making its storage free of charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * t_info is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: INCLUDE values has nulls
+	 * 14th bit: INCLUDE values has var-length attributes
+	 * 13-0 bit: nextOffset i.e. number of next tuple in chain on a page,
+	 * 			 or InvalidOffsetNumber
+	 * ---------------
+	 */
+	unsigned short t_info;	/* nextOffset for linking tuples in a chain on a leaf
+							   page, and additional info for INCLUDE attributes */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* key column data follows */
+	/* nullmask of INCLUDE values follows if there are nulls in INCLUDE attributes*/
+	/* INCLUDE columns data follow if any */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
@@ -361,8 +297,25 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
 #define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
 #define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
 #define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
+							*(Datum *) SGLTDATAPTR(x) : \
+							PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Macros to access nextOffset and bit fields inside t_info independently.
+ */
+#define SGLT_GET_OFFSET(spgLeafTuple)	( (spgLeafTuple)->t_info & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(spgLeafTuple) \
+	( (bool)((spgLeafTuple)->t_info >> 15) )
+#define SGLT_GET_CONTAINSVARATT(spgLeafTuple) \
+	( (bool)(((spgLeafTuple)->t_info & 0x4000) >> 14) )
+#define SGLT_SET_OFFSET(spgLeafTuple, offsetNumber) \
+	( (spgLeafTuple)->t_info = \
+	((spgLeafTuple)->t_info & 0xC000) | ((offsetNumber) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(spgLeafTuple, is_null) \
+	( (spgLeafTuple)->t_info = \
+	((uint16)(bool)(is_null) << 15) | ((spgLeafTuple)->t_info & 0x3FFF) )
+#define SGLT_SET_CONTAINSVARATT(spgLeafTuple, is_varatt) \
+	( (spgLeafTuple)->t_info = \
+	((uint16)(bool)(is_varatt) << 14) | ((spgLeafTuple)->t_info & 0xBFFF) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -372,14 +325,14 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
  * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
  * field, to satisfy some Asserts that we make when replacing a leaf tuple
  * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
+ * We don't use t_info, but it's needed to align the pointer field.
  * pointer and xid are only valid when tupstate = REDIRECT.
  */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;
-	OffsetNumber nextOffset;	/* not used in dead tuples */
+	unsigned short t_info;		/* not used in dead tuples */
 	ItemPointerData pointer;	/* redirection inside index */
 	TransactionId xid;			/* ID of xact that inserted this tuple */
 } SpGistDeadTupleData;
@@ -394,7 +347,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +362,105 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+
+/*
+ * Private state of an index scan
+ */
+typedef struct SpGistScanOpaqueData
+{
+	SpGistState state;			/* see above */
+	pairingheap *scanQueue;		/* queue of to be visited items */
+	MemoryContext tempCxt;		/* short-lived memory context */
+	MemoryContext traversalCxt; /* single scan lifetime memory context */
+
+	/* Control flags showing whether to search nulls and/or non-nulls */
+	bool		searchNulls;	/* scan matches (all) null entries */
+	bool		searchNonNulls; /* scan matches (some) non-null entries */
+
+	/* Index quals to be passed to opclass (null-related quals removed) */
+	int			numberOfKeys;	/* number of index qualifier conditions */
+	ScanKey		keyData;		/* array of index qualifier descriptors */
+	int			numberOfOrderBys;	/* number of ordering operators */
+	int			numberOfNonNullOrderBys;	/* number of ordering operators
+											 * with non-NULL arguments */
+	ScanKey		orderByData;	/* array of ordering op descriptors */
+	Oid		   *orderByTypes;	/* array of ordering op return types */
+	int		   *nonNullOrderByOffsets;	/* array of offset of non-NULL
+										 * ordering keys in the original array */
+	Oid			indexCollation; /* collation of index column */
+
+	/* Opclass defined functions: */
+	FmgrInfo	innerConsistentFn;
+	FmgrInfo	leafConsistentFn;
+
+	/* Pre-allocated workspace arrays: */
+	double	   *zeroDistances;
+	double	   *infDistances;
+
+	/* These fields are only used in amgetbitmap scans: */
+	TIDBitmap  *tbm;			/* bitmap being filled */
+	int64		ntids;			/* number of TIDs passed to bitmap */
+
+	/* These fields are only used in amgettuple scans: */
+	bool		want_itup;		/* are we reconstructing tuples? */
+	TupleDesc	indexTupDesc;	/* if so, tuple descriptor for them */
+	int			nPtrs;			/* number of TIDs found on current page */
+	int			iPtr;			/* index for scanning through same */
+	ItemPointerData heapPtrs[MaxIndexTuplesPerPage];	/* TIDs from cur page */
+	bool		recheck[MaxIndexTuplesPerPage]; /* their recheck flags */
+	bool		recheckDistances[MaxIndexTuplesPerPage];	/* distance recheck
+															 * flags */
+	HeapTuple	reconTups[MaxIndexTuplesPerPage];	/* reconstructed tuples */
+
+	/* distances (for recheck) */
+	IndexOrderByDistance *distances[MaxIndexTuplesPerPage];
+
+	/*
+	 * Note: using MaxIndexTuplesPerPage above is a bit hokey since
+	 * SpGistLeafTuples aren't exactly IndexTuples; however, they are larger,
+	 * so this is safe.
+	 */
+} SpGistScanOpaqueData;
+
+typedef SpGistScanOpaqueData *SpGistScanOpaque;
+
+/*
+ * This struct is what we actually keep in index->rd_amcache.  It includes
+ * static configuration information as well as the lastUsedPages cache.
+ */
+typedef struct SpGistCache
+{
+	spgConfigOut config;		/* filled in by opclass config method */
+
+	SpGistTypeDesc attType;		/* type of values to be indexed/restored */
+	SpGistTypeDesc attLeafType; /* type of leaf-tuple values */
+	SpGistTypeDesc attPrefixType;	/* type of inner-tuple prefix values */
+	SpGistTypeDesc attLabelType;	/* type of node label values */
+	TupleDesc	includeTupdesc;
+
+	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+} SpGistCache;
+
 /*
  * XLOG stuff
  */
@@ -456,9 +507,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern unsigned int spgLeafTupleSize(SpGistState *state, Datum *datum, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +518,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void spgDeformLeafTuple(SpGistLeafTuple tup, SpGistState *state,
+								  Datum *datum, bool *isnull, bool key_value_isnull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +538,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..93e6a43b6d 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -169,9 +169,9 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  hash   | bogus         | 
  spgist | can_order     | f
  spgist | can_unique    | f
- spgist | can_multi_col | f
+ spgist | can_multi_col | t
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..86510687c7 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -349,14 +349,13 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 
 DROP TABLE tbl;
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..213cce5c7c
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,143 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 026ea880cd..985458a1a8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 979d926119..f3df961535 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..44b340053b 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -182,7 +182,7 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 DROP TABLE tbl;
 
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..38ace74d4e
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,84 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
-- 
2.28.0

v1-0002-Add-VACUUM-ANALYZE-to-index-including-test.patchapplication/octet-stream; name=v1-0002-Add-VACUUM-ANALYZE-to-index-including-test.patchDownload
From eb0ed1054b766bd110b0d1675a93065c0185a60a Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 27 Aug 2020 19:55:37 +0400
Subject: [PATCH v1] Add VACUUM ANALYZE to index including test

---
 src/test/regress/expected/index_including.out | 1 +
 src/test/regress/sql/index_including.sql      | 1 +
 2 files changed, 2 insertions(+)

diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..6a2a13ffa2 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -146,6 +146,7 @@ select * from tbl where (c1,c2,c3) < (2,5,1);
 
 -- row comparison that compares high key at page boundary
 SET enable_seqscan = off;
+VACUUM ANALYZE tbl;
 explain (costs off)
 select * from tbl where (c1,c2,c3) < (262,1,1) limit 1;
                      QUERY PLAN                     
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..1f300fe3b6 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -78,6 +78,7 @@ select * from tbl where (c1,c2,c3) < (2,5,1);
 select * from tbl where (c1,c2,c3) < (2,5,1);
 -- row comparison that compares high key at page boundary
 SET enable_seqscan = off;
+VACUUM ANALYZE tbl;
 explain (costs off)
 select * from tbl where (c1,c2,c3) < (262,1,1) limit 1;
 select * from tbl where (c1,c2,c3) < (262,1,1) limit 1;
-- 
2.28.0

#17Tom Lane
tgl@sss.pgh.pa.us
In reply to: Pavel Borisov (#16)
Re: [PATCH] Covering SPGiST index

Pavel Borisov <pashkin.elfe@gmail.com> writes:

[ v10-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patch ]

I've started to review this, and I've got to say that I really disagree
with this choice:

+ * If there are INCLUDE columns, they are stored after a key value, each
+ * starting from its own typalign boundary. Unlike IndexTuple, first INCLUDE
+ * value does not need to start from MAXALIGN boundary, so SPGiST uses private
+ * routines to access them.

This seems to require far more new code than it could possibly be worth,
because most of the time anything you could save here is just going
to disappear into end-of-tuple alignment space anyway -- recall that
the overall index tuple length is going to be MAXALIGN'd no matter what.
I think you should yank this out and try to rely on standard tuple
construction/deconstruction code instead.

I also find it unacceptable that you stuck a tuple descriptor pointer into
the rd_amcache structure. The relcache only supports that being a flat
blob of memory. I see that you tried to hack around that by having
spgGetCache reconstruct a new tupdesc every time through, but (a) that's
actually worse than having no cache at all, and (b) spgGetCache doesn't
really know much about the longevity of the memory context it's called in.
This could easily lead to dangling tuple pointers, serious memory bloat
from repeated tupdesc construction, or quite possibly both. Safer would
be to build the tupdesc during initSpGistState(), or maybe just make it
on-demand. In view of the previous point, I'm also wondering if there's
any way to get the relcache's regular rd_att tupdesc to be useful here,
so we don't have to build one during scans at all.

(I wondered for a bit about whether you could keep a long-lived private
tupdesc in the relcache's rd_indexcxt context. But it looks like
relcache.c sometimes resets rd_amcache without also clearing the
rd_indexcxt, so that would probably lead to leakage.)

regards, tom lane

#18Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Tom Lane (#17)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

I've started to review this, and I've got to say that I really disagree
with this choice:

+ * If there are INCLUDE columns, they are stored after a key value, each
+ * starting from its own typalign boundary. Unlike IndexTuple, first
INCLUDE
+ * value does not need to start from MAXALIGN boundary, so SPGiST uses
private
+ * routines to access them.

This seems to require far more new code than it could possibly be worth,
because most of the time anything you could save here is just going
to disappear into end-of-tuple alignment space anyway -- recall that
the overall index tuple length is going to be MAXALIGN'd no matter what.
I think you should yank this out and try to rely on standard tuple
construction/deconstruction code instead.

I'd say that much of the SELECT performance gain of SP-GiST over GiST is
due to its lightweight pages, each containing more tuples so we can have
less page fetches. And this is the main goal of having lightweight tuples.
PFA my performance measurements for box+cidr selects, with gist and spgist
indexes built on box key-column and cidr (optionally) include column.

The way that seems acceptable to me is to add (optional) nulls mask into
the end of existing style SpGistLeafTuple header and use indextuple
routines to attach attributes after it. In this case, we can reduce the
amount of code at the cost of adding one extra MAXALIGN size to the overall
tuple size on 32-bit arch as now tuple header size of 12 bit already fits 3
MAXALIGNS (on 64 bit the header now is shorter than 2 maxaligns (12 bytes
of 16) and nulls mask will be free of cost). If you mean this I try to make
changes soon. What do you think of it?

I also find it unacceptable that you stuck a tuple descriptor pointer into

the rd_amcache structure. The relcache only supports that being a flat
blob of memory. I see that you tried to hack around that by having
spgGetCache reconstruct a new tupdesc every time through, but (a) that's
actually worse than having no cache at all, and (b) spgGetCache doesn't
really know much about the longevity of the memory context it's called in.
This could easily lead to dangling tuple pointers, serious memory bloat
from repeated tupdesc construction, or quite possibly both. Safer would
be to build the tupdesc during initSpGistState(), or maybe just make it
on-demand. In view of the previous point, I'm also wondering if there's
any way to get the relcache's regular rd_att tupdesc to be useful here,
so we don't have to build one during scans at all.

(I wondered for a bit about whether you could keep a long-lived private
tupdesc in the relcache's rd_indexcxt context. But it looks like
relcache.c sometimes resets rd_amcache without also clearing the
rd_indexcxt, so that would probably lead to leakage.)

I will consider this for sure, thanks.

Attachments:

for_site_spgist_gist_covering_time_by_rows2_lines.pngimage/png; name=for_site_spgist_gist_covering_time_by_rows2_lines.pngDownload
�PNG


IHDR{4��iCCPkCGColorSpaceGenericRGB8��U]hU>�sg#$�Sl4�t�?
%
�V4�����6n�I6�"�d������83���OEP|1������ (��>�/�
%�� (>���P�����;3�i���e�|����{��g����X����-2�s���=+�����WQ+]�L6Ow�[�C�{_�������F qb�������U�vz��?�Z�b��1@�/z��c��s>~�if�,���USj������F�1��_�Mj�����b�u���p�a��m�h��m�����>��a\�+5%��Q�K���F��km}������?�������D\���������!~��6�,�-��7��S��������v��5Z��;���[���r�mS�����5��{yD���yH�}r�9��|����-���������FA������Jj�I.��[/�]m���K7�K���R��D��r��Y�Q��O�-����Q��|�|�6���
�	(�0��
MXd(@��h��2��_�f��<�:����������_�����*d�>������e���\c?~,7?& ���^2I��q2"y�<M���d���JlE^<7����3R��E�9���`�3*L\S��,��#�)�]���_�\�,7Q����W��_���2�+�j���W��r�Z���L��lXswUm��������q��WF~������]<Yo.F���j�VN�D������,�'}(����}�}�}�}�]�;�����.ps_��j�Z�{y�g��k�J!#lr�6�Qa2�'cBQ�������/�=c���\�.V����M�UUT�p�)VoM8�A�$Cd��6T��W��"�O�RiS;S���A���v�m����n�R��c�}Y�:n�
�wK��b�6*��������L�hS��mZ������2�[.G����?���.�������#n���8�������H|�������2x~�����s��-��7;����t�>@��g���|U\�@IDATx��x����W�K��{�6�6��0�a��&$�G!$�����.���@�����.5�@z�1�cLq��{�-[V��������]�������<��9g��9�[�_������@@@@@ &c��x(@@@@0�@@@@� �/�GC@@@@� @@@@�a��Xx���ill��Gq�3$$$����u�Hbb���x_�\s�[�y�v$))IZZZ��G����*�� ��w�?Z[[���z��V�;xk�� ��:�Fu�999~�q�W f�555��\�s�E�����=�Kv���|��u ��n���dIII���:ww�����TQQ�>|�(�{V���]���\�g��xg������{��g���KSSS�t:N{���j<�������<v�08�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/@0�_1� � � � ������ � � � �1/��O�"� � ��L���M���G�[[��!C%)�q%!��!@��v,�"� � ��(p��O�������HO����F����e,�k�	bG�_�����I@@@�nx�����?}���zyi��n�'�#�t]�`�
i@@�y������OW�<���:�2
@�%@�]��� � � ���nl��%�on�����}}�(@p�@w�z� � ��J@o�q��%�G:�u�� �n�����F��yw��;@@@ ��]����{��ili�{� ���VM���r������~*-���	��o��C�zE�m�&O?���X�Brsse��Er��W{�s� � � ��oo�W}�FM�pp�@XG�}?������X~�������.�������Djjj�z������j���F���'���{�
^�@@��-GK�����y�[	#m d@
�5x��A��e�\u�Ur������3�����#G���5k���zJ���7�|��5��w��W�s�='���.$�K � � �@l	����_}�g��oM�&����mje
�� �B�����%!!�;�O{�i�:eee�W�\i������c���RQQa�� � � ��\���U~���r�����Y�����%�II�rFZ8� ����yyy�`�y��g$##�X���G�1c�����
����
���c���#� � ��X����+��oiu����=.�D5�����1���T��|�o�d�$�C_�T�����o�~Q��+��@���!���w�������G>����|���^Po�aN��N����S_H_z�%#?q�D�<y��]HII�{�[���HR���u��H�HLL�����z����������w|��H��sjj*������=�������{��y�<����~Z-��������������+����(OOI��OP?��=�B"=��l()�����������y�_I �@8��#������	����V2��z4�u�]'��s��1��o`���777{�����3"�����O��$]��:PAr��~W�/��'�CO�[�_��;�+��+�C�F�{��+�Wt�/�F��
��������Z���C?p�%2�W/oyZ�����j�W�<F�o��C�v��UW����?c�r��+���~(z���w�-0�TO�������W_�o�Q���������)X'0��a���m��fd+++E�B'�����B�:-�R@�����'�[@�+=��N-�Mr���===��-�~M���=�K�������m�UF���I�R���3�� ������\�������y�t��3ef�>����������gQ�]�����\�����?�e��(EyF��y�K������bC �C���_/���4��f5c���t*P������c���~�����@@@ �-����^��*�gN�����M7���Cb����8l��K���Y5���DR ����{�!5������zZ���k����(�>}����=���+V��F����@@@����|��?r���(50�ws�{���O4k�:���e�}��=���}]��� I��-Zd|���[Eo��w�^���`l�q���z:��|�}��W�^-O>��\{���)��D�� � � �@,<�z���u��Q��2�p�d��d�������u�<�f���������C���B"#�5���'w�y��u�]���}�xb����~�3c�.(,,46���<������3g�\v�e��� � � �@
|�o���F��S��`��/��y�b��=���b9o����r��e>�}���2\�qOB� ��~�i��;��u����}������< �f���_~YJ���zj�^8�� � � �}�c�^�����n�}��0�\�s��6�1��8��M?nx�i�|w��2w�p3� �@D"Y��}J:(��o�@U8� � � �A�5����,

�+/;V��8�R����;;]�ez��/yC�%�����C��3f��8F".�`��� � � �q&��F����R).+�<���}�����������@=R�Am`Y�>����&���d��}��yyr������@7	t���/ � � �@7
���������;ee���.�����{������r����&�S��;Q��t��V�Ty�
��)�������N����(&��7�x@m�����^d@�%�}�wIg� � � �@����!���'��S����A�`S{@=�������;��5��4Y�Z$35E&��m�t@����w:�/(w�����'����i����������
��z �@��Z��@@@�	lW�0��l�O�~9�<����v$����������M���
�q��-,�y�G����\ �1�< � � �����z���W��&K
=2���GGSJ����M�-�&>���������������c�| ����H�@@@'v�}]�UVX��0@n:���`3��L��m7���6����#E2R��dIS�2s^�N�=Q�S�[���e'�?Q��2mim���{KVjj��O{ �@H����@@@��^�T���S���\���$������z�@)I���	����G<�9]�	�8#(�d�����% w"���������I��:�7''�i�!�� ���Ag@@@����y�<�v��1D�����##�R���=��v��$�1��
�h�{^`������%%)�S�� �a &hn� � ��K`������,�������Q��>�)��M@��H���q�z���<[)Y@�p
XWo
��� � � �@�Jkj���^s`N���O�!�G�����@�����Z��JI��9��22 �� ~s�� � �t������krD�i�����g��:}l�l4n=j
�*(���	@ �#���@@@��	���wdM�!K{�{����-Y �#��]�nly2 �� �i:.D@@�#�������6Z:���&�p�d��Z����[���U}���6�`W���������@@@�����O���}�����{,��=zX����u{z���:�lh�4?��2 �@�)y�� � ��@`_y������G���Of�%g
l.
�qjb�O;:h_�OW�_�S�@��00���@@�@]S�|�����:K{�G����L���*c�X��w��<(//�S�C��A�Q�����yf@@�	�[�Z&JJ,�rZ�^�������2�o
�}����P���@� @�x\� � �DJ�?�D�(�n�}AF�<��BIK���^�Y���j� � Q~n� � �t\�������[.LIL��T��Ov��<��`���cOE��V����A���H�so@@@������,Y"�-?D���sej�~l������TZ*��MH�q[�@�K�`w��. � � b=���_}Yj�--_9i���i�-e���#
�i�a�:��))�?'�^�< �@�|�rG�#�@@@���aw��7dO�u�����-j�_�����G[n?Zm���`)#� 9����� � � �@���Cyo�K������W�*N�r-C�q�WqY���Y���A���H��� � ��#���m���U�Z�j��.\,�YY��pd������[� �@����& � � v�M�G���/���o��+��z�������SO&!��G��{�=A@@,eu�r���H}s��������Q�,e���$&�������9� ^����n � � �@PM--���_�C����g�M?n�y��,��@#��IVjj����@��)@@@ R�}�]���A����!w�_(��a7P���,�� �
��x
t@@8%���
��
N����T���INZ��<��P�� �@�v��+@@@���:tP~��;�vT��dh����HeFJ��"�� �97�B@@B.PR]%7���4��Z�������!C-e���p$_
�F:�P� � ��W�A��{�
�����x����w�uSJIr��dfJ���uSW� ��p��

 � � �@Xn]��l<r�r�1��r��s-en��$%9vc����
J;F! ���/�� � ��xh���������HO�-�5��m��`�$!��O����	=B@@�8�p�����,O��F��w�"�������z 	@�}��N� � ������r���7������??�9��K��2���t�[�/ ��)��,8B@@�&P��(����T64X�y�����&Z���IIt^pdA���J@�@� � � �ahS#����%��x�������[�9�R�������y�������'@ ���_@@@ ���X����r�^YY��N�5KEd��8���.x5tp ��B! � � �=K���?�������T0�SpLaQ4t�>"�q)@0._;� � ���v���l�R�[�r�y2�wo�r�8G��[_�BX�� � � �@8�����j����f����<E�8f��������.�.,�)�@��t�{� � � ������7^�U���<s�@����,e���`�K7Tn@n��� ��{��]�@@�Q�����<���t����.�����oY~F��Y�$A}�@�)}�i��H�@@@G�m�$O�[k9���bl����n)���Mg��t��Q�,y2 ��HvWw�
 � � ;kKJ������@w��/#
|���`lQ�|��k��������-]�� �@\
����C#� � �@w����^U�Z�f\?��;lxw��������+'N���p@��0����� � �X[����*:hN�&�;}���c@�]�`�s@@�7=�w���c����;��g��
@�p�2�@@@�����5�������KK�?_x�d��Z�� � ��P� � � ��'w}���Y������<K9@�%@0\��@@bZ`e������������y�d��A�22 ��S�`8�� � ���@mS�\��+RQ_oy��G���&O���A@ ��-��@@@ ������-[*���<��^��W��g)#� �@$FB�{"� � �@���g���;vX��03SXt��&%[�� � 	��P�� � � �w��V~ly���D����Wv��� �� )y�� � �Q-���L���%>�p��sdr��>� � )����� � � ��

j���Eo�aNWN�$��;�\�1 � �W@@@@ �Z��?^������t{F���/����A@�
��� � �D��=}(��k�o�����$Y��GB@�m|wr��? � � �Z���n�GV���/#9Y�t�b���a)'� ��[��M�@@p���#G���o������dLa�O9 ��E��[��@@@�
���^{EZZ,}����e����22 ��M�����A@@W	4���_UJ��-�:g������22 ��Q���
}B@@����we��C����S���@,�d@p�@7��� � ��xz�:yn�K_rRS��j����4K9@�*@��o�~!� � �@D>;x@~��{�>��~w/X(Cz����A@����v� � �DD�`U����k���j���f�%����A@������ � ��U���F���?�x}����F��k�N���A@ �����@@�n���k���-o-���6����o��k)#� �@���7E?@@@�[��j��6�3���gOz��?-Z,�����nC@ :���^"� � �@�69"��[#�m�.M�-~[���3�oN����@@������ � ��L@o��l�yb�Yu�P�����K��8��zT@@����v� � ��D���N���Q�Z�N�Tls@N��=d��7t��48`]N"� 
��-�G@@�����G��~/o�*�j��@�����M�l�U� �Q%���RT���������p	E��������i7ddd����h���x�Cbb������x�p��'���effJ��B���|v��Y���SRR���


���8�i4��jQ�|���|��S�x���o1C���|��f��2��0`]���������U=7��IIIF���{vv���tJ &F�ot�'�S���E9j����*��9����:5���L�����I��9�������:P���|t��h�|�jO�=����D���[�y'�z�����Q,/m�"�JJ���6��I?���^���q�InZ�Q7Z�n���K�z������������NnlA0�O��!����M@@��;�_[�����?@�>i�Z�o�0��].* ����y�< � ��*P���y|�����F�.=F��7IFE�4_��	@� U@@@�������:,m�W�e��	�K���j�,	@�x �o��F@@ ^W�}��e��I������-W��(Ij]W ��.@0���� � �D��A�i�����{��������C,�d@�g~�o�gG@@ J�����\"Uj�?s�a���� #� ���@@�N���}*�<h���~�����[�� � �@� � � �@�	�-)�?������T�s�����@@�*�@�9@@p�@�����K�H�m��_�w����qq�� �� 9{�� � ���{����
�U��+G����A@�SOYp� � �.xm�6yq�K����g�c)#� �V�Vr � � �B��UU����[z���(w�_(Yj�? �� ���3 � � ����V��Z��J��gN��8C&��m.�@:�P� � ���������Z:tz��r����22 � �,@���R@@�&�U*��b�^�#��KkK�_>Yi����&w��/�		�r2 � �,��\L) � � Z�������%�ee����k��;d��+�%�eTA�������FM��IM�mi��O�_�9O�d��*�@
��I@@�����ryx�*���M�l������i|x����"=�3���Y���K����s��q�`�HOu>#� �@�@�
 � �t\`S�y���e��bi�����ZMS���48/O~v�9N�(C@��p
@@:.�r�>�?��H���t�����F�@@�c;�Em@@p�#����C�>�
G�8�8U4F��7�w��y�Q���=*{+*�uO��dd�������@@ (�A1Q	@@�[Z���[�����r�*F���w�Z���i�dLa�O�Z5�w��c���a���'R�6IJL���T��������k(@@���D-@@0	�z���A[�Z�����XS���2�q�5S����<�ISNO����������Lg8D@��
�� �#� � Geu��������uR�����sRS��	��I�� 3�o=N � �@��~c�� � �@�����GV��m�(
j���T��e��?A�T�� �� �w@@@p��V�A���>���o����4�G����r�������@@�=��.�	 � ���x�^�������{�i|�^r���2w�p�}�@@�}��N� � �A���MY��X���W��/��
7:������v���.k�lf��A���3X�� � y���@@�N	<�a�<�e���[�Z&;����g����Doy{MjM�W�m��V}.;��[]��[0b��W��o=N � �����}�@@�x�n���^%�i�z4���}|��j���7��kVKIu����8%1I.;V�Qk����H � ]��}�[@@�������m���W�{V�>i��8s�d���O���:yb�ZyJ}T44X��3�j����|��4Y���$@�N�����5 � �@��������_���>�v���s��v�9{�9PYi������R����
33��3��KG����4�(G@�( %/�n"� � `X�{�j�s���Jkj�Er��J���K��ze���j9k���S�|����c��GO�R��@@ �F�;�	@@�P`�������������}$�lXo9�3GU��_[T$�N�fl������ !� �@,	����� � ���@uc���������7������G��_,_.;��Y��3g� ��6]f
d?E@bH�`�L@@ >���[�Z��yu�����'������&�d����Y�����2�woo � �c���d � �.����eiq������,�X�;���;�-��),��y����$����c�Z���K��$5�W����/�q��Y�� � �c��x � ����;o��O��W���^����%�u��z�����\3o�K���@��{��RVW'z���$�`��@�x��<�e�@@����i����K>��{�H�
���a�����=����?H � ��o�N<5 � �]�NM���G�5��X���,�a��wp�2�����8� �q,@0�_>�� � ^�!={��p��#>e�
�T�����,���3��R� �����|�<4 � �@$j��|n����>e�
>=p@*,���g�H@�V�`��z@@ �U����������i���Ov�L�����~��@�h mo��"� �D��Sp������3��:o��n"����6J@��q��yH@@7T64�t�B�
�TZ�Sn/XSrH���Z��
����2@�[�q��yp@@�pT5Z��������9��yi�u�o���������@@<=|F@@��� ����w���e;wX�=o�0IJ��y
@p�'G
@@�����;�45Jy}�'��yS�9XUe)g���� � �S � ��J����_���d��_�}������JI����p� �� ��� � ��N �@}���o�{�7m�s����$��9� ���58F@@���v6�j���RVg��W��u���8^f�*������ � ���� � �!�����kmk��������o������Q�h@�A�`<�e�@@ �U�5{ge����,�z�x�%�3o�v���������S�@@��@2�#� �D����W����9�<$�**"�,�

�>����#GZ�>;p@Jkj�ez���G�x��`����<@@�=��	q@@ ���w}��P��*�v�3OI�m^��>87-U��)	����������wNNL�9C����@h_�`�F�@@�2�7�oL����&�[�Q�N#33���<�4>����I��g���j� 	@����hQ@@ *t`���&�L�5sSO����W:$����Xm��:t���yL��x�A@���D-@@�	��y�\������������jmfta��@��T�����i����%&�'�,)�.o��i��k�?������� ��"@0V�$�� �D��k�7���Z&������%r��KSKK�{�����fqY�OY8�k�M@t����3��oW��[�7�o_q�h�D@p ��B � �@�<�v�5���:c�sY(�����B�t����k�]0r��]�x�>K���#,y2 � �@���� � �@�
��k�5��J���.�kn@�����=�"���XdGV6X�;{F���US{����b[�P�!!� ��������@@� N8�R3-)I�B���Kd�~��9}�]���E#��m���C�9`����-*����~�s@$@0��@@B+`�f������z�lTk
����d?�������e���������OX��$z��9�G���#G�O[��1���A@:&@�c^�F@hG�x]�\��+�N��`�=���bz�n(�u?]k��e���EmP�����_
W:$�|�_�:������1���z���4`]����p�O@�����q � ����ew�#��];��~�y�yYu���fK�o�O_�TZS#G���6W|,pp���k�������T����DNZ��L�4�R�3C�z���|�r
@@�`+E=@@�v���}���[7{�km�N��'�v��^������o�R�X�h��C�X|r��@Mu�i`Nj�������S6����	 � �1���6 � W:�V�0z�����>�/-)�[�>X��������M~`��N��C��!�F�;'&$HV�u��nJ�>>��S��@@�����"� �1*�w�����e��c�_�?Yn�����N��5�f�v�u��7�������f��:^����{�k�9]\�� ��^�t*��]�����WY�&��S����23��t�k�N��9@@:#p������k@@bR����4���<���T���7%O��k/%����M��ZMc�6�e�@;��6�����
����{���?v��������'I��OQ���9���[�X�V>���������BS�OWJV�����o���K�Z{����$=��})@@�	��D���� ������*���u���x���?��S�@��w
-��{u%c��/����y���l��k��?r�r�YYw�*��������`�g�T����o�$O�['�*+�����bjR�7o>�����0�\�1 � �%�w���@@��]X��`}sr|��)0��3�<�64�n����Cz���=zxne|6o��������n��^p���?�r�Sj4���jK�'�j����?&g=�W�����R�(�_�7W���$@@ ��2�@@�L���lAF���i�$��?�z[8�WUZ�8 7���N��'�7yk�N�Tg]��S�J�����3�����9�e���"����r��s�e��7��#O��Sp�
��>B�:T���/���-m�A@�S�`w��6 � �>�x���9>�9 ��6���n��_�Z�LI�7w����3����u������I�������������^��M��SE=��-�=�>���������N����Oe
@@�n �M�4� ����yoG��|���Pm����%�)�:PW`�hc�
��.�Q,���<����CO��j�9������r���F�u�K��:<�=����+WN�$��
�?k�y��@�n��-L� � �@
�G�������3���_���Z�TR]%�`�n�n�O�M*��K��y��w���m���EE�p�Hy�x���+�����M�����?�~�-�z��__$�j)'� �DR�M@"���@@�
�~�� ^�.���u�A�V���:�F�N��w��O�,��Z?5x��$��~����+�����t�
��'��~��3fZ�?���7�t����{��	#�H��BB@\ ���:E@@�%�j��#P�����M@�77j2�9=%��G���&�z=��%+5�(����8^�������c}P��%_?�R����S.=F^0������(�T1>�$&�"�@@�	Dt��o��@��7��@@ t�@^W~.3�lQ;{��`��_�C�O�c�i����|�m-�E#GIj������w�5�-G������N�^��k1!� ����SN7�O�������5E���Lf��%�\r�L�2�{�m����O?-+V��\����E��������@@�{�,WK
&�S�����_�\g�����s]���r��z����;�g���cF{�s:�k	~I�����uN������"$@@���_c�����w����7�!��s�4����r�-R[[k����^n��V�����7O<��<��ca��@@ppX��R�AM�]�������R�3�����.L�`�]���'�((��|���%c�z����;�t�5u�K���>���g��< � I������_~Y~����&N�(���/$G�p��{�O=�������7�,�F�2�]y����s�I�Z��� � ��:�:~Nw/���d�F�z����kva`������y� #�����i����k�T��J�.��\���w&!� �nkp������i�< iii����].\h�\�Rf��)���f��-�e�O�@@�Q�����s��Eg��S�<����SW��=�?�����y=$�������=e�>K�t�����_f�)�$@@��a]����2t�P��w�<���R\\,#F�����Nz�����d��	���B#L-��I�jz���[�l^^��������$��SO���U����~W�����L���<%���$��[�L�z��]%��L�+��!k���e�U.Q}�2'=�7���TI����v��������W��?�p������?xhA��]����WobO��������z�aj4�5���tP0��=�D�g�mE�[:�G�=+E����I��_I �@$������)M�n��<x��3F�|�M���O��������1�O��r\-,�I;w��_��WF��+��/���S|����O�Ar��(���������#==�����������~�gu�;J7���������������V�+�Q�m����=}�N�(�y�c���t��3c��q�T���;�~��������r�T���3�����mE����*K��5������UO=AZ�U\�vW�5�7�����~�������+_��\u�U�� �\�F��E�#��7��xL�<�XOP��AE\$�N@��XUU�i����(8ohh��{�ph��7X���c�@IDAT�M���m�?REEE|o�6��6�������#�����l:[��������[z#�T�~����ES�o����v�vjW���3`���X�ZB��t���lS��a��������}����/�u�Z��.?����_��{�{�^G]o�Hr��g�����������tJ ���j���-X���Y=%x����y�f�L3�����?��QN$@@����3�����QwU�?=k�|c���.��N1�����^��it��k�����pcs�S�A��7���Nk���w�	�#k�)*!� �.�& z-?�4']���������g���j	@@ 8��SA?sP��5�����V�@�@�n��	HK��*�
]���y��SPoD��N�yjzr�;�q�@@ ���6m�1Mq���^C=n��U2n�8�l����wn1� �b�
c����G{��@@ �@�m�y�
:��`��c.o0��<��i����v�������>��3�q@n�O��N���MD��a��D ���@X�S����c�����.�6m�����w�a��5�|C���/6��w�}����W��O>)�^{���W�!� ��#0J��0����������@���@���m�k�gjc���Z{������;�_�glO�j}O5��R\��2 � �a]P�y��FPo����~���-C�1��t���������?/z��9s��e�]f��@@������������r����d�y�6�	�������k�:
js���z���]������
dP�<������F�w������V��~-~����@@�X{P��w��w��Xo���W/�Y�f;�����0��3I � �0�������y���@?
h��9hnJW���e.2�?��G�{�Eo������KO�b��O����@��W'L����S�==����N � ����}���7a��X����!� �@w
�7���������Vt���/��T�����#����&e�u���f9��Y��KS�=#k���������<S&��#�����46����A@���X069y*@@�=�A|�|G{h�l�i7{����2��G�X;k����Q�9������tj+�5{ggK����=}|�������< � �c���\ � ��{�|  ��:�M���4�X����}��RU����e���h�h�����LI��,-�����#22|��@�g�S?=���� ����}����tr���4h
p��(x�i�/�|";���g7��_�w���(���~.UkI�;��oms���sp\m&BB@8%�x��#@@�X���s
�u�y����
�	Hz��y��]�����S9��}�TZS�-r�$P��{! � �@	�����"� �@|	�G
��e��h��<TU%G��]�i�u����
�>q/���^lNz�_�;�\�1 � �L��� � �@�
�~���~���cr�[��L�����s��c8�t��{�����&����T����T�r�y�S��������d��#b�q���gp��2@@ �o�gF@��p
���B��w�SO���������W^6�j�:�c����6n��������4u.X�gg�U������Wu'���Z � �c���\ � �>#m���;\b1�������Rf���+Q�g��3��{����~�����~�C�$�!� �Q/����< � �,`h�����\��v���>�O�y��nl��{D��6��� +%%$HVj�,>Bn:�A^E5@@ ~����I@@ ���y�o�.�I��x�ITA��'�J~�|���Y�>M���1��W���	
>�v��u�,(�����
��}��ss�y{�� �D���{et@b]�I�}7���3r�+>�e���$'vl���?�`itXc�������	HmSsD^�����A�e����?7E@�U�`��9�� �1+��M��?��Gjj�������:������}T�c�����g�������y����$&�-g�+I���s�@p�@�~�����{ � �@,T:�d[��Qw���)�
��#������`�.�e�*������?��#��p�s/@�%F����Y@@ &&��������CU�r���RR]%�;}�\>�4�z�{��lp��b�>s���j����]#e'G	ZNvsfx~~7���@@�� ���'C@�����C����G��zo]���gd���C�?��0>S�����L������E4���C��o��t9�'�8�'���r- ���S������ � �B���V�^�*/�){}�V�2s��@o��D-���=9����Y����G���O�������p@hO�`{B�G@�,��,�����Dzd������ATV;�����
���Q�>mz
�P�����I�	�`��a�=;�.!� ��0�� � ����[}s��������N�7������^{U��~��2���r��6����''���pw�N���QRB�$��~���=�@'�@@�`�D@@ �NS����.�-�i��)�����w�`��Ix��K��#���z����+[Z
}�x2�ho�wV����&	@@�sL��W!� ��&��9{L�>J/�6���<�U{}���{�n{��
������9��/�� ����J��7�� � �@�8��3Z�sJ����~���}�`C��.�-j��������u����|����4`����p�S/(C@�W�`��;z� �1*(�gyd[\,MM��u�Y�1e�Z�����\v�� ��ng�z:o���y��CUUR^W���W'Lt,�@@ 8�_�w-�@@�A��!0wb��5�g�W�N��>��i�����?�u�Y_0�34�m;vT��{��Q��3�U9F@����Q@�n�\Mc�O`�������z�;��kv��������T�����6~��,-)I~;w��� � �@�v��+@@�np	����n��T56x�Q���=�{`@(NS��^�[JK����-&�?3SR��/^"g$^�X���C����\+��MB�
�B@�F�5��U�� � --��}';�4���7�g}�� ��)����{��a����������d�8G�� � �@�:KZB@B"�4��!(��h�l.=b�s��C��������6��
I		r���F+���e|����_��r@@?��P� �DJ�i
@�F�S���W�����Go��t�Rf�������g�t��_�u5�/?�-� � �@ ��$]�9@@�nhjm�i��g�U�����~����������fK��3�jS���w�mh@@�&��6� � �@(���[^��E��Qo�9}����Zx�������4�K�=�]~�RAff�#��{����j��N�^�^�SX��/��A(C@�6��.��V4� ����k���O�.�>��#���^����=��xF�U76�O<���#:6X�F�������o�����KBz^�0�n�������@�U%},������9S�S�7�U��3 �@<����3"� �@���v�������Y.3V�{�y����O>���x�>K��IP�ct&�Q|zjm���W^�/�DF�=r0��������v��R�I ��;���$m�k�N���F����h�)�<I
�N����$A}} !�t�����e@��]���mo��}�g7n�s��9C�z���RS,��*x�/�
2:3%W_�������=�����oJC����YgIg���(Gw������?OO��d��o�,9;�D0p�D�s�qc%)5�S�� �! D�@@���S^����J�W|(�**T �E���K2�o?K������#G�eCz����+������++Kv;��r�CFO�mj��T^=��]�$\S�'��-��'W�1��	(B�X&��\]#%|d|h�����NQ#�G��	����$@����W"� �@����c/��Cv��
|OM�]����y}�n[�.�4��Tm����?-�uu��g�Kc~2:�X�����f��f�F�l��2�O_����q$�5p`���U�zt����X-��c��A=mX}�t�].@�Y�`<�}�@�����S��t������{=��Rk�-x�1�#�������l����jg��0��U��������hV;��S���2������rl�iQ�i7�_��o�f|?��Q]�S8e��'K���6C@ ������@@ 8�z��rX���$���=y~��y{�'8h�l����S�D��S�������m��P�������~}e���1��� �U`�����h�ck�K�Zs5�T�����=/�jTO��)ES�H��u{�!	� )�{R�f�����; ��E�U|CyEj�>)-��`n���s�3�d3���i]A�u�� ^G���&�){ij
���8I�������Q�Xh��Sss���t�j��1��+����{�������R{���~�������r�C�K������Q��*(��8IJK��@�� ����C@�P8�6[a�
x�$[������Y�!��3@�������3\��S��f(��MQH �@Se�!%'���gr��1����Su�-'F�%X�c��/���|��5�rx�J�C�LP���?m�����Xd�Im�/>�R�D��(~yt@�#��_M��?=�=s�|V�;��__$��4��`��27���Ay=��y��������[e���8���s��+�Ou���Q*�Xi8h���"8����5�5���G��3�
��E��!����f�S���Vy\E$o��A����X$�WQ{�p�Z�Q���8 ��K�>�O��>�N���o�n�$��[T �g
���w�))r��A�����z�>��u2�Wo���3f�}+>��[���$I��M���]Iyjz�W'L�J\�1&�hh���GM����_8�����44H��M�Q����=����~�SQ������?��Y��k��Et@P�D$!��"@0V�$�� ��&���,Q;������}�v�����m�d��iT��nwW�F��x�,�:]���w��J�_����bP�Q}m4�����4_�t���������2����%�7Q�������A�{_}�h+�Gc��,T�{���"� �@4
���F�@@ ���~����`Uc��O����v�,e�i��cK�n���S�
��T�P��&�bL��`w�����:�9f��1��/������KO�6�w&5������5>t������x#�G�/I�l,�%u@ �#�� ��\�<r����x�-�>���G�9��i��>g�E���P�J\����@�@�Zw���:7�#=�
�9{��C.^lT�?z��:�zc��R��X����/AZ���GV~j|x��7�;��cRJ�$�$K�Z�!!)Q*w�R��k$�_?9���%)5�s�@����)0�]�� � ���V��'��X!5M��}����h�C����%}�4E�� :4������G�����]0[��6]�'�u��4��#�d������k�B'��@�=����5U���u��Q�zs��`�mT�����E�**-AA{���Y}��2����O�G�&@0l��@�I`O�q�����r{�����[~�i������TlH���������=_m~�gn�c�s� ��
@�kI���>�f���U�l�f5Jp�1BP��w�tji5����@�"M � �@�	��eK����h����V4�����������k�_0���F���w.�TZ��|�=�X�	q���o�RC�	Hw�&�)���'r���E*v�<1BPM.]�Fm,r�C�n��.���@�(!� ���?��Rq;��kVs��#�<�+�Zy�����������Z0|���'���nRB��s�N����7}av�*�C�8hTSc-Im����e)����X������/���~z�����Ar���������=���,u�5����$A��+-L�k���hxT��1,@0�_.�� �����o�������E����x��K�)��m��d�g����*�rOA�:���)r����"��S���I��
�Q�r��1�_�$!�v���9�����2E�>L�>�����o����z�}��w�c�#�_t����@�[�`w�> �D��=|���hV>#���$C�2��Ej4M��e��F��5S���k���'��7�7?g�0����~�sh���LQ�#�CZ_�NW�Xu���<@ ��Uo�{��@@?5j���?�@~��+����V��z������������f)�oG�Q���x�c�]._;���S���>C
32�N����;G��)�04UZG(�qsCq\Q_!�.����ON�P4O �@��i:.D@�H	��ug?���57]xq�Fy���'}\4��E��3���������19�0b�S�#����+ol��xI�����o/����F&^0r���]�I �@ ��h�$��s�7��3�oD�H����s9u@�n``���0 �t���z���*������v�/��]G�w�D.��(���3��@��B{��$�E�;�V��3e�����z�;E9 �h��L���B�^��|��=�����=��_/X�������]�c?��{�@�*��������K�<-K��G�uW���oU���2u�5c[#m� �!��i�z��'N���t�x|��
2�����c�?�yp��$7v��xx��y����4�}�����T��2@��v30�#� �z}�syH���.������x]����c��|Xg�S���������5�H���5��Ws����|���__��dcK�OY{�8gN{U���P��H�0�F��1)`���eB	^�Pe����j���o��#By+�B:-@��t\� �@�
���0����S���R��:�lz`�-�X�P��f,���(�5���|�������{����O�����&��#F����=d�yHB�����#o�Wr����d���r��K,ed@�H
��>�F@ J�������4/�Z%d9[\-d���!c`�Rgs�>]5y�$$�N��c�|5��DJV#���s��}�3=�4��A��
��1-`��.���{F�����=�$?=�w'`K%2 �@�X�4���@�X���!�j�������3d��8]�����P��7�U�S�5u�cs������|O�;&�����s��^aJR����M��	�T��ws�Z��A	����z���R�E��^K�S�R��wHfJ���Z �t��n��Y@b] �,��@����(�P�s�P;�NR�x�����?�T��;�/PS��d����KI ���w�QT]��������
H�&UP�(X�w}�O���X�&R��.���zHB
����f3;;�lB��M�����;w����M�}�&`	���4d$�|ID�����������e���E�&�����:�u-A�}�`V%`�.�����1&��`%��6$��v4<�o����d�������a���x'%�0��nXCL�������]��{�:���@)$�w�d��v�������"<��q&�4�F�"1-�dm��HpWh<H�72&�����}��L�	0&�����H�������TSa-?���U+�%����l����T(���u�B��g;x��=��PFx��Q�@������|�fL��H��Z������p�<B�B���[��vK�B~����%]�`L�X�X,���L�	0&�J�������s�L�^��E���L��>���H�S�|��?���-��E��Js��AC4\��
��K���
��H��~��F�����9N���?���Y���x<EUr��@��V<���*�>����.�\@�&�WY�%�����+�I���aL�x�X<���L�	0&�J�����6W�GE2��!>���o/v^�7g'L��O<;�dd����>y�>j�V;�W��[��Z�S Fj���I"��j*���8��$\���c�����~�a_[�A�}q��~a����e�.����u�c.�����������h�^x�����`L�	�h�w����`L�	hC��r	]k�����]"���h�v&
O�!g�&=G8������(jQ���n���W7D%��xR�)��J"a��Qc��2��G6k��W�����������>y��)%:����7���6�K���[�x�(������s�h��qsrC����W�>�����e�����S3d�|`L@��zT��	0&���7k�t�T ���^~���)��(�E����h)���L��q2�&m�q�
B���~�y��[T�����y�FM���3�j�vE�4�	0;'���N���l��.W��������;�2W_���S�tD%F����.^��o)�����j��������=_�`L�	X��V��cL�	0&PRh��
�h����v`����Q����+����H���0I���ZtR��]�����iXs�]�����<��`cL�	�F@��G}�-^��������&��q�z�w���+�"���s��N��mz�u�}u
by�U��J���(iX,iw����`L��h%�b����q!�}��'N����D����<"�Y2�Q������{
|}&���	*��$��j-)�&�m����tB�V��;8]�������}�i���!�3�no�l�O�}�+j���L�	0�K��{kyaL�	0&��K��!������F����?2�nI�b��[TV��bc��,�O>��
M*��S���`�F 6���k�[���x����FL�H��[��s�\}�{��YP��Z%����������:,����k,�����3&����x�Ys�A��EAH��z�N��&�Zs.�k�K.
P��U��}���h]�*��5����L�	0�� b^�:|$�C�o{eaS�X��C�K���^������{���(��_]xx����9����-oo�{�0����C,��M�)3&��(l�"#���U�MNB�jA���W�s ��`�F^{u�jl8���h<w�T^x�{�8�Q���'1&��A�*��;����#�3"Ui��>�$����yg�/B��l��,,���XoQ5��q���������k<��/=�]��3�eL�	�Z,��[�gL�	0&�M !5�.0��t�~?|c[�2��mh�h��l����~�����1-[���zce��7p_�*�GR������\���
�y��K4?qv9//,<$_�pg&��@Q �����8t��|\���~'�q�3�$�,��;1>@���/���W� �Tp���F>�j�9��v���x���~�����E��?�`L �X�?3>�	0&��@�"p3!^
dj���\�S�B�J����?������9L��UW����Z���8������*W6}v �����4=�-L�	0�b$p+1
Go���p��!\���=�'��yG<v�	������9���p\�(����)�XJ���{.���8D��z����S�W� ���5e��^ncL�	0��`�`��,&��`%�@eo�����SD���x�u�"aq����**c��9��i~�*���6Pa�hV��2&`o�O��3~��T!���!��	��/�IL��@ e\��v�S���Y��#4������
e*�k���P��>U��r��"���`y`0OD��	0&��@�&@�h����NJ2,�i��*��b<ETL���D�4N���{� ,6C�w��1�
~fL�	X�	~����}$��	~���������!��W�����#Sx���O���g�b�rV�sx`&��d�C���sKC�F�1m������`V!P"@���]|pa+<���Lh����WWW�����t��wK+���<N��{U��o�yjsE*��@s
����QQ����W?�����n������YH�d�n\�v��}w������,�,����+�����q�"&���p!O�g��A'QHHk�����=pe������.��$��iy�m�8�5����%�^�+�f���y������\Y������2��g��tM�������E��K��?�^���B6&���5	��>L���@`Mx%�Z�"y;3��;L���Y������)�7�|�l�^�����Ep�-��+��:���d�)F���Pm�1G�����A�;zX�����!�X�2���PQ����Iw��N��Qh�j�{���_�<�q+��Il{��a��AF<
���;�� S�lw�<3~?h?������h�}�������?�Jk_r@���3<�T��~�~�h��x���w���`���Xu�c4�v��df�	��������x��X��,W���� ���`V$`����.�K���%�o�
�gQ��~�W�����	K���pX�(�����^�/���a�������������v���E�Q:m���h�D��

��;�I��z�9[�����3g~�nHI����4k��AA����2�����,R�^_wo	�d�q�w������E�����=d����-����D�o��jh�+�g5��wOrb_-!�5z��z{��>d��0�����+����p��B�]'��e��Z��}��T��0���v&��J���V��cL�	0&P�	P�:�_0����5b��+Wp�v���buh���	ii��m�)[�,�r^^���lg:�`�jx�}{9�����`L�^��w��A�r���	~�e��e�Vh�]^�����U�]/]�y���f����5����=|�P�o_��X�x�,�	sD�������	F}y�	0&��K�@����1&���{u��Sx��p�6�H6��'e?]|�L^�d��^J�|&/��B���\�mJ!��:�u�S����w����aL�� ����C��J�%hj�
F�������r+�q������������^A;N��jh�����p�!���C�q�A����p�����yo��&��(T,*N�	0&��@�'��bE|��&�Y-C����?����<x5��e���S����HEC�2����(JR��%�B��d�<M&�l�@Q	~>�>���������w.����e��`�h��8sI��>G���Qn��}JL�l"o�,���`�K�����WgL�	0&`�:��
!���
��'�b����$�)�z�P�w��U����D��(;;���]�3&�L��X-*��~-*�_���\���+8����}��1� �������,���X�Z�m\Y��+6����-XwaL�	�%��w�v9}�4`L�	0&P\r����@�(*Ui'5#=���?�}���NYO8;:�������������5�$�����W�!�]�vR�4Mje=i�	8=s��_�,3����������~=��?���D�?���88����)_x,>�	0&�
�����GaL�	0&`��b^��R�(����{��'������� Q|�uU'��~
^��bD�(��mg{���Q�''��vf�3aL����������ClJ��)7��Ukp����r+��85xT(�&/=��BD,$s����@r�-8{�/F<�H!��a�`&�`����L�	0&����@v�u����3g�$�<W�������T6���5���1�slq��&�F��y���(Z��,YA��c8:�;��6��������z#����p� �r^~���Ww��8��	0&�������G`L�	0&P���N]87_=��6%tx�������Cr;Dx]��"#M�g���h%�����Z���U�k�k3&Pp�"�)+H��?����|�1��j��h����E���`L�t`�t�g^%`L�	0�"'�at!���pUoD&&5S^�y���);�		�[�*>���Wo��o*{���������(�lM�Shg�����?qf�ld$���:�W����W��(���ML�	0�M��}{yqL�	0&����*�]%7�?u�~�8n4�O�n	}z���z���FB_��UQKT�����5.��`L�������q~�Z�����x��o�������{5��������s��	0&�JK�}�U0&��(v�y��'��	�c��>-�imQ�S�<g�{�`|��SNo1&P"ddf ,�"V�Z�M�7 .9�����-]h���hU�5ZTn%-���k����
=�#��E��#��88;!��Ah8n,\������L�	0&P:�X:�3��	0&��@��T��tq1�������i�&�[��n` ��Z�z�/8��Ll��5j��G&�{���A��K 3+�b�p:��D�AH�i�D� %=�@k.[-���Wt��2�����u:.���r.(�F���E��_�w� �v�aL�	0��I���y�y�L�	0&�
�@n9�EE���y��"6�`�}!��Rezq��$���IXv�9���3HJK��N.�)�����}�c/���N��4B��"��b-�<�ZVn}W�+?-���t�_����DZ|������Q�^{$�1&��P����g&��`����|�4X��G���M��n�HLO���3��h��U���
w�>�m�]���F��S|Ht�whn&��A����	��I�D~Mz\��*�F��Z���'��#2>RT����[��g�i)��
��D!������o��=8:�{�	��n6l�x�A��c<t0��*6&��`j�?��o3&��`&����Hv��a��&�H^-�j���������m�����k�$E����WE5t�
��*����wI�z��^��uo2�vx#�~$�-?�3��&����r=��Agg)D�{�c@����h@���3sy��;�.���S�s�Q�aG�
��W��p" `L�	0=,�Q�6&��`�D`����i�^8����o7�B�h���#P+�Y��o����O���m���aM���g7"�s�_���]6���R3�6oX�	�����"/cZfZ�&���R]0!5A|����m�����=�2	W'WP�����^7
�5D���pv,��J�{z�8�`!��3��=�l��o��:�Cll,�����h0�aL�	0�M�x�W+�hyqL�	0&P�	\�����K�qWM���������+�QX�C�S5V���e\]u���d�\���R����;gn�/����g�k�;[
��~���s�@� ����n)*�R�N!�m�+��f��kL�:b���C�~�j���^D���bN��+����yV��&/��j]�;�mL�	0&�L�h���`L�	�'��?1Axf��u����hj0!��CyjFRS����t��j�����x1W2'Z��f-�9s�m�z��kH�m��6gGv����}W������
��T����R�OdD�r�����7D=!����v?E>����ClH�.3'w7�5�F���x��.��`L��l�?���2&��(\.�G�%��Z�#r[�.��A������x>�1�;p�|\���]��e��

�x�is<V�!������|��9��
���i�0m�zQ\���&@^z������##S?�g����D�ahW���{�(�FP�^j������b�����n�=��r^�Q�L<\��4{.N����8�'������qu�F�����+�Lx������`L�	�#��92���`L �E��M���Wxg�q�����Re���8f���=�y� >��a=�y'i
wD'%a������R<Sw��$
�3Kr�;���'��V�%��'v�\X��8�D M#<e	q���	�]�v�[�Q�����:���A���N9��]#���	��Y��\�;�>P�l]<�U>��V5��w���2{.B����c�ie��
���/#�YS����`L�	����F�'0&��0&�(B{��5W���	!p���hV�R������wC��d��]���>&�I�\�x�;�a#��c*�$Q�TD��H\r�}����%$/l���}�t%��5�eC�/Sk����q6*���c�Gt�����1��bh�ayz����g�|���|R�c\�������
���_�.���{M[o��v=���3�nF�N��l������)+�v�F&`�����i�&���T�&88�E���Ms���r��h���n�::tH����;��+�d!��_�;w����?��Z�8E���~���`��nO�	0&������Kp����_X�;�>#C��6�a.?���X��{��Wn���_��.r�a����i8G�q*2R�k�a&7�Q�"��'>t����J��^�%g������8L��+��\
���nuz��v/��W9��&mg��ehs�t@��NR,���'E�0	A~�
}�m#���FxI9�;ugg�yr�
//�>�����������O�D@�������
������[o�e\�r�G����������p���K�:u�`��->|8�n�
�^��]���Rl���=��92� `%�lO�	0&��@�Pq��opK���o�����������F���>���7�tN|������p����VrhEh������Y	�}�N����
���u�?�����|��S��?ZMzb�����8���/a��M���i���Ee��L���c���:<�����"��F/��3���#F�@����?�����(��t�R)�M�8q����&MB��mq��5\�zU���Y��[��k�E���_���D|��vF�����{/�:`L�	��E���"�E1w�����-�F��
�qIx��z��a���u��|���h���}�.�Q5�������}��"�y~�����b{�if+Y�5a�\x��x�1|��+�D�������<�����1'��ngMc��kH���@9����!-G)�v��)���-\�S�GzB����k�@��_A��mt�s#�7�N��!��F�������Gk�~���[7�\������kXZ�*U0s�L��-�6a���3�1H���EH����������3���`Fl����x�	0&����(��.&Z��_n���Q�9�A�
���������+����q��u����p���-@IDAT���qe�H��@H�+�<��%DYx�8���-�p��E�.���iC�������������?`u�J��;��}<����?��<��s���$�e�P@G�r�d��Z��o�G����#��.��h8~,j`���]72A�<���<��Q�.��9r7ETy���Q�F����4i"��W� �Q
�P���������	0�\	��H1��KL.��K���`L�	d�15�Az!�������aw����Hx���M�k���������%�[�7j�j*�k]��c=&E@DVN��H�_��~_$$���Bc���m�/�@����7v�
�tn��hxV(o�0�����0)����W.���������qp��������L�>	�;w�|����G���7���������W7Z��S�����
�1�x�	0�\	X��f��%?~�a���e��E���A��k��1�
&��`L�����������\��V�+�����o�����^
} ���k������������hO7h��}��	L��������:��f����.��"���,r����
�$*��a�����n���F�w�d��n*��@(<�M�6�1cj����?��V�B��r��a�c�����s#`�C�"�e��a��Ap������Pz��-�����O<�<(�gj<
`L�	0&PX>����Sd��N5kbD��r�F"�f�����L����r�@O��Y��e''�	�5c+j��j��~'���>6���]���7������k�>�m��Yf��m]�����e���/����tO��K�W^B��u�s#(I�N�������5j���BCC�~���acL�z,I��)>,�H��T��*���������pA�="U��SBN6&��`L��P�������I�f�WQXu�P7=E�[,�R������q%�,��U����yG��������_��u���Z�7Q3��EcZ�)|�n�]��l�0��!��-��������^Z�H�����I�7�</^��"Am����'�(/ `�#��He��}���^C��M��V�^-�(�k���
���C��9_�	0&�����l����n���N�m�(�dZ������gr�<o�������)�~��n�
e*b����s�.�����r�����h�!�	�o���?���-f������<D6&P�t��Yy��W��o�������'O��>��tttiB�ke�N O��|���Q�bE9Yr�]�~=p�}�@}RS�s��`L�	0&`s�X�;.]��y����m��W���#`Z�\On�{	����=W��r�D\]0��H�j9�.�����s�4RD�s�e���s��=],:������f>�4n�fo�����4x\&`����"�O��'�|R��*�N�4	-Z��Z��u����^O�	�$y
���B�}�����K��a����_b%a7��������K^`L�	0�E�bL4~��G���g�t��7���J�=��d����f����-����D�:0�����kt�+��*�U�tB���*M�@�R��v=�M�	��Q������������L�dt�w�@)"@Uz��|���R,_�<*W�l ��L���}�����n;S��3`�S�q�J���cG�8qB�g6n�8y�O>�_|��G�-��`L�	0&`[U�c��J�ys
�e��K����+�0L�7X*F�V��6�]6'4�J�����?�?��Q��BVU�jx���h^��UVrc��\����w`�'�����pd�w�>~BwDG�[����h��(8{z���F&P	����y���c�q��f&`K,I��
>�/���7~��<���r��m������e@[Z��	0&����(.������P$�J�4�g;��������Rl,���d���U��sQg1y��8z�����p����c�D�a��*jK��D��������6m�[��+�H��*
�h�����\Z��]�2�;>�f�N�W��&�~���`L�8	X$�����?���3�"���J��M�2EV&a��	0&������%����7Ln�����<<�D*��d��}�YN�`�������%m���8������-�^��������b��Q�Ly�N������#���i$&[986�,�����_���Op-4������ancL�	0&`S,�����W*+}��	0&���=):`'m�
7�_��U?���S��]�����#�p@��������d\���+��=_���3�@���Y���x��[hQ�:���yh��
�?��5��e��"->.e���s'S����g/.._��[���U��h��3�5�D�36&��`�@��w�����A\�zT	X�(��Y�fz���	0&��(F�:�N�Hz��P���D5QC��r%jM�c�
�tT/���Y��7���q*���B�V����g�<���<���2D��}�.]���p,k�F;�X �_^�W6lDjl��u��j
��F�����k���`L��X$R��������>b0WB|�	0&�J[r8�,��~����k5&���(_K����m�����z�6�����?����y�2�������]=��oAw��	IQ����uH�v=�a�5�}��Z���y���`L�	������K������U��3���G���`L���YA����s�����C�^B�f����\�;��v�@�Sw�-6�k�NN���x�V�|�WT������+D6������L9������u��{&���D�����p���-����`L�	�*�<���x���`��Q�9s�������`L�	�!����;��Rb���-��;w)����eH��)�`J��70#3�o�����{�P��2�|�P��:���d���;�T��+��W�Z�R�v�i��
@�Z,@!���\��M�~��y�Y�"����
m��w��4��7�`L�	�+�<@///�\9��c�7����`�%�x)���:p'%E�����������E�z�=���^U��
w4���+Wu�|����z��q���tB�3��~,�X��w�����m����Q��x����X.N.&�m�!��y$��M��s�=�n(�|+Z�����y���#E���w�
��e.>����3��������ATgcL�	0&P��)�~�{���9s0i�$�7x6&��`L�`�����h�;z���F��j���\����l��{VHT��M.��al�%d�i����ds���	�)9�����_��k�Mn�x���&���pc�.��9���\��J�����!�A����(r�:�������}Ggd��-���"�`
U}1[o����;�G���Q���Xz���	0&��@I ��H����_��{wt��c��EPP�tJ�W�.B�16&��`L@|��"��aa��/Nx�)VZ����*��
����II
�y���D��G�:��L�������q��xu���8eB��oc`��&����
����
�\]
��;{���ld�t2�A��:��E�B$,_+)�U���H�
[4��J8����{^���mz����x&PL,cbb�.\����������*U�8q���|�	0&���=�~�"v���`_j�����7o�Z���I�$�����E�Ls}�����#x�5)g��H�����c.�����S�Wl��b#�#���/ ,��Q_'Gg|�y"���a�n;)�q�u���T+��	��n�����>���_A=���~�����<&���g	��G���_�F���C�Po�=��^3�1&����O��G[7�{L�������mu����{K�,�V�Y�r?nB������l/���a$\�f@�����e�#�������$|��0���K�{V<��x�\ynNn���K�������| |�&9�@h��^x�&�V�ePq������|j�(�8|"`L 7w��9M��h��'O�a���5����SLMM�.���`���v�,�&`L�	0���y�>���;�o��k�02�;"7����b���p��z���u���C���7p<E���f-������$���l��������I�N��XV��� ��P�
0\��2q�����e�O�HO��'G'�k�F���_BL�q�/�2���whV��Q{�����s������+�k�n����u����z�@>��#3-Mn��3���+6#C��Wx�~4}�E��jO/
�+(B6l@�n�p��y��U+�+e��^��M��1c�.���f&L@�N�0r�H<��S �l�����R���x�	DDD`��M����>�c�=�!C�X��;�.�<@GGG���~Y��`L�	�t^����*3I|����[������E��ZNx�Yb��]��W��N?t�����yB������,$��0�=n���i4��B�Cd4 ��<�*���������������a����
�7DlR,~��#����V�{���>?�n��V�&������6u�_u�O�h��3��\��{"��|�	0��I�r��R�������E������sr;����8*
������u��cT��G���(�]�I O�Yx<��� ����HdcL�	0&PR	�$.�0��*������
8v�&�D���m��f9<��9�N��mQ����}P�Ly��[�+�^bb�� ������;���R��#P}�&��^��m��#�o�����E!�
o1���� %M�xEzV:���u�v�21������n���[G�#->�h���3��&��@a�tf�g��xH����y����%����H���qc����x���,��}l��EjU�&r����k�������0�}$�`e=���bL�	0%���@IS�vs+�L�t�������qqH"��C%��u����{E����H�\�kBC�z�/R,�m,��FG��QA-��U�|�-\��%���|�����}�x��%xg��|~#~�o*�Z��^7��c7�b��i�s�8�U����%�_���m����i~�{�/K|��7�h��K!�lL�	�,�E��:�^F�X����Q�B]����S�E �qXX�~�i���U,����1}�t	�Br[�n-u
j���(���H Cx��)��+�������c��9r[�c��mX�n������e�1�g��qH����c�����/������~)�`�>��C�e��7o�,�@���Q�����/�Z�j���o M�E(	��1�4i"s�S�V��'��E��5k�����6N=��|��'��|y#'�?��@��������0g\�ngL�	0{!�/���)]��rff��R�0��^CLrN�_rZ:^_�� �(u�<R�66]8/��$
~t�}�	�U����)����|+��T���)r����x���$_\��B!x��_�����"D������w����'�>��_���a������U���c�]��+yW���������sl�c�����z-.�\���/k��Op��=ck�����E`��c�C|�i�V����k�P<T]��^#	g�����/�+�T#��n������o�������o��'ND��� �'��_�.^t��]z��w_�V��.���W�M�6�|u���Kg��?�\���=(�����L�����z�����K�.�h����U��P/^���qcEJ:7�nz�#G�`���Rh$�����7��|��x������~�)���+Po��U�6�G�&1���'���~��57E�
��6l���C}�m���r��d�aw,�5k&���V��A����q&��`V%����(�����gZ���-�w�(����<�/Hy�Qe���4o����*�RYD�*�l�Z�"���A��N|s�O��"#��BE���W�����A3��������j�����a�����7}�>Bh���9�\�$B�30k�T��juv�`�(��5E�������/".9V�$�)G�����n���d��y��/��p,�(�^2���_��Q�'������'����t��������q:�N�>go���m�,����n���
�,����5��U��z���`�"0c�:t�N���I����3g$�=���x��ge�|$��Fo;t� @*��e&g����u�����d��{��}��� $�����h���-[f������<�6n��.]���,�A���H���m$F��E��G�(y	�=��#�P�_MK(���B��I�`��g!������
J�%�}0}��R���`L�	����
��V�����"�~�W��������?�F9�Fo����zWLhQ��������\���E������.]eUd�{�r��CY���;(�6�����uW�%M���Kmb^�Mb������C�����Q�swC��n��g{����|�Q�i�g�M�d�_��7�y�����Xyz��OTv����:�ov|�w�G�����#F��Wn���:��q�&�����NHM�^���|������tQ�������j
n��/b����U�__[^��	0'@^q����4]����H�#o9���
�$j���#1�Bc�Fa�m��|�H��\E�����Oz8p@z�)����G9�����%v��Q����p�fMFJ(��_���[���<*����Rn�2"�4��j*iMz�"�$z��/�@�]��	0&�J3��"O�����M�(�^x��!��8�����;u6��	Q��s��`1ixG{��U�t��GQT-��H�K�s��,�uj~��9r����	���J�BU_���}i���iG��Q����M�6��L�\��q�<�)����wED&D��]SL�iR2R@��Wh�m��^�^xY���Q^������wMx��[�����	�*����������^�<�@~�����Nm����@�e���G�F}��Yx��8��VI|�ZK�1g��
���9S�����g�
H��Nb�y	k.\@�wLk<����>
���b�0��	�e�6�h��'?F��j#�G%����g�!����H���6m��L(�!=HD�<y��c�8���S7���`��nO�	0&�,'P�lY�9;t��Jo���.?��h�����R8��h���3z��ME��d�����~6��x!C���K����A���T���Ha���d����\�����]\G��� ��S������9��a�9^lc.��������aTm�*5�9!&�`���U�����_������CRDd�Kq_�T������
m������;0&`����I�N0
�_�F��s�D�w�@X������P�O�}���y��a��i27
�\y�K�4�2G!Rzxx���&]SE�_�Q�-��Tp��U8u?*�Q�vm�D����5�Z�����AA���Zj���/�)�!���\�+W�����HbQ��Z�~}e�����������`L�"�EH�����Z|���v0~U}|s=7/�@���:lQ�o��s�w�<Pu`��	��7��s�6?�������\��,����M�}����!��6;_��h��f�7�lT(�^�ltJ��}�������M��I`|F��������[��~��r�Z���#�������h��S1��-�����a�p������G�/mZ�������������@[��^9��Jz�Px���G���k���������l�?YD�P���E����M�� S�5��N6j~P�PQ�Xk��P]��B���i��2�<�(����=������QX.��S������~[�l1�F�si>�Q���;Jv��W���T���}�XN�*:�AZ3��`�~���	0&�, ���?�����",8������/���M�3��t�M���
#��e}�q":������3�Z����R�������+�vVT��iH��zr�;}�����,�������5��^�y�Xzfz���r��������������C`p������Q�����Pi��&v2�Spy�z���V>�G�|��3�z����]M^z�V-�C?OE����Y�>ecL�	6
�������~��7|��'����A��^�]x#������/�_�{��)��*��xds����rG��B}�@�<5j����$����!�T���������K1e�����\}��e�����Ty�����_J�<Z��!����uT��7�����*+9i	)����_��T�������$�*v��e)��������{�x�L�	0&`�3z����a���?�Q������{���X�I�I�Z���MEXL���Y��/���(��qG�:�pu{�L�
���&�4v�m3ZZ����wN����`��?P�?'���3���t�#R%��:��5OmU�����A�A�g�
�������=������o�^���D����kQ���3l(�����E�Q��Q���3&�
����W�����;�?>~��'�EG�u����>��v�
�a�$���T��}��h��9��yY,Do�G}T�t	"�Ym�Ny���sTd������?�.#F����~��'���Kb���k
��
��C���w�E�5d%��
�tA*�JB������S��F����P4��6i�D���<��Y#ERe��
H��yWQ���m�������bL�	0&`!�,�3,E��9�U<�,'@��m�B�B��E��n�	bQ��v����9=-���]�}z�Z��i*fX�bz��w
�]�2�����.�.��)��������w���C��i���C�>a����p���fh�mn������z-d^��y����-;�_�GQ�~���'0���@�8/��P�����
c'�/
ID#O?��]�B��[�j��d�)��w�����U��p`���p\=����EH�D&L� ������������-�A�y��9$�Qn@*��6�/HB&y�Q�>�h�z
�=�5*n�6w���2�����R84=n��
3i>Z#!q���(+�kk����`�^���	0&�����M�2�/v�����$��9(���'G���6���<�J������.[�����_�G��szB"�+��<j���>d�m���VX=(i�i:���'o��.M�����NL�:���n�����?���F�6}R�����]4%��W�mU|���^cA[w��Y�u+�bU;w��^]H�����l�������#o:��G��$~u��)�iP�_u�mg�����_��!�)HU����XFB`
����Q(���[�����H���<)��5�=H�����>�o�_����3��M�CNq�J����1&��(�H�#���p�������G�����Fq�K�~Q-Wk��o�M@MN@���q��b�s��1����N��t!�x����+�}m~>��V�����T�dd�1t���8���a����g���$r�����#���������B�[��"�7K���e>�j"�g=����"t^k��L�	�?�������:t�nTL������C��[T�8p ~��,^�O<��'xQ�����<y�E��,�k���K�P��W��Xr���Z�*�d4)��)����1&��@���i��3�{97?�)i}�7h��"!5yH��o����
uE�{6����a���j���R�����^|l!~����Q�l]�?���Z��3L=��������+9�in�
D9��s�Q��Y��Xk9�vzF<��"����� o���-�A=�I���~����q&����	�#����"��������yE51
�%M���4$����	�H�2{�!B�S@�����+`L�	0�P��������\D�����^u�!21AVn�)�V�����M���xWi�5m����a��<����/�/8���y���6g�6��n���);�6t=y
�� 8���<#�B����	�x���s�
WQ�wd��Fm%e'����)�oZ������*F������	�����u^^^R���<)����Fk���G�A\]]���6wM��HU��Yz/��B�*�D��m�J��&V�V������`L��	h���DE���y���2���h�	V��#�7�����X��u���|����	iB�s��V�.��!���k�
��.=��������s����p�2E;.����B#���eCZ.E@���A�?E�F����V�b	W.*����=����g�G�����"U9����G����U����Q��,��
�q��*�
6���.\���TT�
_$''����yPB���z
��o�9���
U�����g�}�E���=�|s��A�0���{5��������0�|������Z��x\�M��E����|{&�7����U��6�������`L�$�zAY�F�IBE��U���t/u}��G�J��(JN�p��V�L�=[�c25�(\��&�����JQ<d��W����_y��O
m����q�(��]��E����KQ[x��fF���vlT�qn���1����VQa;]����c���i���I����{�h���S�n?%6�6m�|���h��5k��(��C����_�����`L��	P��#GZ\P�/*�A��fT ����PW����=�4i(������.!�|u��F��~��t+&�[�y��@��7�|c�[��[,Ri�7�x���L���}{_O�l�F�������.����L�	0&�l����X�a�Lo��!���pvt�C���qe�24��D���p�T�+�YZ~UQ8"%#%���P@s�O���@�'����:�[�G�4}3w��E��u"Q��fg��G}N�4-O����d8?��
�~���8yZ^��-���_
�9PX�9��V@m�
z��{i82�{�-_)r�%X4e'ww���W����Y��;1&`�H��={��� @K��w���U�-�V�F
4n�Xz����q�e�����gO��{M�o�k�5H��IX]���Y|��
�l�RV��_���D��!)���h�{����cL�	0&�?y3���n\	���Jz�r��8����;n<����Z��O�w���/� �/(�[��,��,�����v��[G���F�~���g��."@
bY�������=�;����^���W����Q�Sh���B�h�vA�1g���.c��C������-����~����7�nY���������U�((G.y_kgn�����p+>����O�)�TXX�6y��X�`�������e��{��u���v������	8T�   �:u��M���+���~��4g�~��m��a��ux���d_���Q�F����C��j�b%=�|}}Q�N����HI��2��h=j��y�\��#{�����_|�ET�VM>�i+--�	�����Gy�����A+n���O������5kb���&a�T��O>A����3-_�����c��yh�������>�m���2d�,�B����~2��'�t�n���i�F�TJ�Oa�t_I�Z�~�r��Lk �N6E��~�����K��zz��t0z�����r~���qi]�2�S���F��8�"@R')��^��@�����r�K�,�G}$E@R���_��k2&��(�{�[���\�)�'�����v���mpq��1:G�B�$`z��b���������j�f���K��e��'�T�Jsv����#xRa���gFy��X>j5<]<
m�l���6T_���M��PD���
�i��>��;���������U|+w|����YX|�	0�B'0e���x^���m��hdy=�(�����^�W��F�[��u��o��4�lt|����8q�k(�	3��_G��
��{w��F!�����^�$@������K����?�s q��)E��y����W/����R|"A���X�j�����&�Tll�\Czz��jG����c��H�!]�BK�
�[o�%����O��W_��x�����<�u��Q��=ez�Y�f����R�6l��Y#���Cm$p�����K����!$����)$D<X�A��i\�'1c�9��^{M�����\GY�Z��c�R��RSS1h� �: 6""B�Y-x�{��G���g�E�����w�������3g��\S��w�i��OgG(����7�}�])zk��wY������Y$n��I��*/�D)�"	�x��=,���6`L�	�=������%�������'���G�*�l�j�P��dm��������P"�G��r-[�5���o���39fR}��Gn
YH�hq��r���v��Xzf��u��v�w�����w�QSs��r^�,�^��
�5��"����i����@G���c�k��)����!j����]����`cL�	�d$����S���A�T777��gn�$�=���R�>>��LkF	�T|�
GP�s5H��+�J"9G��dT%�o�� /9������J�h��e�0	�!��g�y7nD�.]��"#CZ�b$6QAI#��DQ�$#��<I#;y��+I($O=2s��]���L|�)I"��(O�� .$$�k��G��$�������$�t���ptt������
���uwI�<v�����d4g���-\���H�y$����C����'K�T��~6w��}h���^y��L������|�������<���L�cC�"��S$%�nP�z��N�AR����j�#`L�	0&`g"��G�7�+Bz�7�j�%B"W��W8�#��d3&(����7�%���>��ee��]�^7�6�|�C;�����Dh���@���/p��a4���v�@x�F3�����hW�u�1G����]f��_s�S���_��������i3��N\r��#9����od��x����[9G�o+Cx��~�-�_�l4yz�<���(���Q;�0&�J2�#�G�h������8C�r�YF^Z$j���#1�Bc�Fa����y��'���������"�Q?��v���/�c���<����FFE+���B\#!�[�n�_�q�������c*iMz�"����2^n��_��_pp��~��)�XI�!�O1
.��x����4y�����V�?�L�k�������<����q�Y-6��|�t_�3Sm���eaT9V���vi3=�U��������HH�1�dcL�	0&P	�fd`K�i^�G���M^����{]Sy!L�����F��Pvy�V�����2�����X�8���.a��B�<[������W]�A�:p����x#+���d9������3J��l�&��9��<��s{�3%B�����?��s(������y,�Q�&�JgH�S[�J�t���>�eF��3g�4x�Q�n���o��F�f*TP�4<S8��T��>
���b�0��j�e�6����������������={V�������H�4�=l�����<�Z�q��;�m6�O��j���c�chh����}�e[o<rV#�������?��F����@sys������"�)�tm-3�~h��s��Y��b��5��pJ�9Z$�T)��F�v��>��L�	0&����9}�Y|;��f2�2�4�����=v�c����n=��>J��������k���A��.�Tk?��������o����B��Y��	�c.����a�1���y�y��WA�aZB"�\������n�!{;�6�\��E�9q���E�����JN8�%��n�*�����o�����F�p��D��S�Y!�C�Q�aL�	���F�{��0K���^?��D�d�:�����>XG���6�,z���U�3rt���M����h�l#��
i�e�U�!��R�T�Q.:�Q�'���J�FGGk�������|w�4���P; ���������Om��H_$��\�R�>�g	TTD1Zka9}��{D9	���X�MY��-���������?^}��V��r�Q?o�1MN�4�v�5]�.�>���R�5c.�\;fa���H�2e�T�)Qf�*U�k$#WRz�R�6��O���`�������uk��������4�)�%�7h��:u6,�Ld�������\���v�(Y��$��j�xsNm�N9��	G<��3NL��33fae���9#��Lc�����0���Wc����(��T�!�������'�t0�v�W�fQ	Q����ud�������u���^wE�TQ�Ck�������������������j�$��qI�?�)���m��E����������y�C�X�������{n�"u����W�����G�QG�aL�	X�@�*h^���c�}c�����W�>&��7�/��M�]�v��TS�<��D6:�,<�)�w��E��W�&mC	�T�!�����<S�$y�i���+�R�/y��w�S�.��S���Ge�
�CFa�Jq�������~[�l1�F�G�|�\;v���H��'T���M����H�5�Q���;w
G�S��-�=R d�{LES�F=���EE����L!�$tR>F�����Q>H�h���km
1V�R�`������V�T�)�m�@��1U��_r[%�T���N���x��Z���`�%pR�����X}6?��@�XwJ�W�s�
��*�E��=�[c9�2���� /@Z�kkVc����Y�r;�t�*��m���ovK�9�e��E.���	�sB���7Z���3M�G�m�vO-)B����������}��n^`Bj���?�������D���4�X7���x�����C��a�!q;%�p�z��]�U�]z�LX�<��]�6���*����|U=�Mloxb$��� C��n����z/�y]���������
��\[�zy�	0&�%@�\)�y����o���O��O�^�������}�J���
HNNT	���#�;w�l�;����9�C$@�5JzzQa�q���e�a�������K��ER��G�I�R
�R.;���_~)+Z����L��$Z�3o����h^����H$���_~Y>S���?�ggg��������Dk.,�B%T������L�&L�Gj#a��y���|IX��)JX1��*��aIk��%�QiZ�HB'�9����;�`�����^4w��c�6�������b �L�tj��	�t��m	�4)�	�*8�"�9�U�R|��5Y����q��"�zL�	0&P|~��*1�bl��#��!��f9��!��z&n�����N���%�F��&Bo�7��A�>���X�����u+�mm�T���#��V0�DXLi������'U�����xq<��3�@:�Q����^w�������,m�K��G��}���[�!
��������|�u�����$4qD��N(��*�D�6\C�F�A������zR���8�r�u�/��g����Zl�S���I�������\��D����������2g��Z��(Q������4��F�>���M���:�`L���PR�j��Ts���*&A^cT]W�>��#��GyI4"����Bd$������?/����A��H�S{�Q?j�<w=Ig�����?�!F��o��'N9[�pIb���k
�I�:t(�}�]��QCV���H���_��G�#U���g�G�Ca����I�J�X�f�I�I�7 ���U\E�o�3!���q�b)T<�]�C�KvT��<8�~��kI����$VR�C�0�{��yH�Q�/i��G9���xLj�M�����/E��k�^S4.	��J�����2�k�E�� ����(����#���+�K��a�u�|�Y�.�����_�fn��(S�o���+
(I���]%���F�_�������k���g ���Gs�T��>��P����1>��INo��m�{���T��E�����R����fL��G{�k�`�F+�Y�����D%D�%�W�����h�Ux�8�S<TbX�v^�@IDAT}�W_��~B��u�U�
D.������h���a�y'������k�ea�����('!��Z��w�3_?����39�}��P�\}\�n&��8��4��n�g�����:�������,������ �4=����s�;W_��[�����K,�����+�^K���`�6���{!$@H#��n6�^w��d�����{��J��wl�i�����y��F�b�
�����y�����9���?;����C��k;_�V��{������Q����vc�_#���T�(3����5T���AI~�;k�s�K��BE���h�����sOW��*��U�����C�u�>fX
_v�\�?0m,�����pM��a�v} k1��n	�Zi.�o�g�}&/Y����a�W\z�:@$�>^o��2
J��zHZ��@����f8�
xk�l���M������t�?�����+yo����@9����(AP�����~���k��/������&�������d2���'�*����q������u*���*�1.:H�=�33r�O./�� ����S��P(��T_Z����I��bu�7�+`&=<` u�d��+m�7Z�C>��
�����tC�.��o��jF�[d�]���U��w����������%����l��Z���M��}R���w�Gt&>�R�Z��	��Us����1�������n'].�U�u�GZ����cD�$���{��la7^��1m��nM�Q��������*Z����n�.�Z�+��b��I��6yW{�����y�]>�?��������{<�(����=�A�5��z>�x��U
�P(���-���j:�5pu�v������OF��L_�!���x�
��
���$�"���������������%x�3()��\���u��)�1p��h�.���u�IOd�~�����-����G]o��L�>]\������������DPJ4�e�nC"@#��w�y�-0"|�!��� ���8��2��B@!��z�����jyyB�=_�}3?�}w�ZZ}4���Ku��+(�h�\��6��H�Uq�����u7P{���G��3~�F7��������S��������
��:��FRE<e.:���[�	?.�����(��������ET�a������
�Y�[ k�GcM����Y����<
��n�
�]_�O�U�q��JFYb�������9��Jk�����>��K��Bw��l�Y�!w�66�	���t+�zwfY^U���$K�eg�PN�Q�=v��z�����/���X(
�����E��!C�5��S���2nE-{���������7�\Q���~����(���D+x����P�"��6)KO����@J����;��7V=���x��)��_y�M�}���^�k#�2� Xa�T�P(.D@t�`�Z���(�WU�@T�=�x->t�WV2���K�`��#.�=gNS~�y���[�.�[���}X���3������OKb�O����������vuZ2���\���:�2��}U�f��O��"�8Oe�l��j�^48��:"O������:AkG�G�r�<Z�eql�5'���~;j�	@���
5�
������-Bt<�v9�fM#G�Yv`Aq=2�ot(�������s�������#����������b��������=��i���.���~����R������9F������GL+m]��P(.��BR
�EB����
��zE^E�7cxJ�TC��k����./�u�z�
$�&�R�a����"VeP��c�	�v����[y�{#�#>�z����0������Y�%�.c&���c��B@!PS@�;f� 3�(Ie����S�rp�Nx%s-�0��63���]�LJC��&���$Z�����Q�j����t�=�'F�����QU�k�"�p��l�cr�u�A3���Lv�)�u�i\����m��t��+	���S�I1����?�z�+�����z��n�� ���� �=������������P2�)&6OQ�P]<$�5e��&����l&W���V�h�K�"=�{��������$X���*:�oW��8���^g����z��MS~���B�{��J��{|�J��y�����\��5������0p���������������P��?��i�X���B@!P����w���I��j�HF������q"�D��!�cYc9��?�s�;4��!B��-/��q#����"����u����/ZrR^�D�ge
��B�BC�� �@����li��@���N�2�nLtH���*�V����q�Zs���z���		������S�����{���d9����~��Xs3V������C�}!@�5������b�����m��}=�����h���4�����[��p-�4��ke����Pk#W��������F?(��|i	��H��������f�^���s��v%�vn���p;�_;�6}s����5�������p\���3[���&��G���o���|���/�!�/]�V�n|+A�����[��zhj8+����S_�z	��"��P(5�\@2��j�v��x`ZS�Z��U���H����_���_���,�,��<���l8���`Ne����L)��T[��B@!P���#�D��L�XX��&o�<�vp6�V=u�iD/��\.2���>�������I�@e���U����V#����>����V�W���gG�����$��^������:k
a��r�v���Z��������x��.��J������+u"=]k��	��,���geRPkV���J5�����DE[� ����C��~�g��n�1Q{X��������y���,����M/�B#��B_�vl��\��%ne�/�v���}�����w�s+os�����[�:P(
��E`���4j�(B��@�;w���B�!7�L�4I�^aqTU5�;D��%K���|����_�����n��F_UTy% P"�9����t�e����#��;��4�}EoHg\R�M��C��c�R�B}�A�)a�X#
~n��vq�������7yJm�	�{9u���@���
Fu]N�q
C���Y�A���7��-����t��M?���4m�T[T.�+�G���1a(�?4v�.����4(��hodg4�
k�	P&�5�FJ��qq��YsB�����[{�0u��:CZ����0����'�JF��=��Z]\I����������V~�BNx��3�L�S���k�����W����gW��):k������V0������nZ] �hM���	��$��!m�~nQ-JY�<���>�E�E�:]by�R��-�0�R����������5��������d-�[0���]l���q�q��CI���7�M���x�7<��)g�-��f�j�����������(�_��1��tH�D�Z�V
�@I�}]�Yy�K!���K�e�,���m#����1b	^{��e��T�*c]� ��|��v�ZZ�b��y!�/���<��"��W_}��u�i���1� 
��7l� ������.,������!�����\�A�#��������2���`����d�
�Si�������W��y�Y.u"���sU�o��u���v6]�E���F������KW2�w������84>^d��:�UF�u����n���O�U�������,�wc1cl�K�tLs��}_�[����\����%��u#����M���}�������@�F��9@����Cc��&�9�]3�W����d�$������
\��Z��"�._���dR
���X����w�~���G���b���L����������E7�=����<�b"N���+�M�_���	;Y�c�a	�<�<
�up��������OD�Fm�~XR��Lj���������	�t�j�����nEW�g�"�q!�
�!��7IY�F@���o��jz�W_�K�N�D_����'�\������!H]m�������;w�>�@����x�m�	�<���p�fC���P�����^{��
F'N�/yy���F�����������5y�][��?��R0�o�����������O����F��|�w����E@�O��l]���+M��]�{o�����0������k�S� ��4��</?�:�y�L�d x�m|�N��m����������J����]�-����q���@]�3�S\���@��P��*����6:����f�?�����V:k���js�@V�j��L�{�\t�O�����bm����?�-fB�����f�Bh�RW�r��ke���p).��r���e�0��HI�����N0���Sj9v����-��F�;���r('���H�����q|��
�q�^�,c�~����R����:��P&t#8��O��H����ym���?��W��jZ�x���U��U��m��q�g�������?�:�C����5z4?����y�	jp���W[�o��w�%x"o��m�������H��g#\m�?\r�%���OR����&�P|���h��y��x���h��9�������K.���3g���_���q��y�o�c��i�-�[�l������7��=�C���,_��.\��X�>}�����@�e{��7	*zi�x����
� 2C]�����l����I<�y�Bh�?�Pt�9����`p�E�6` 2�����/��"4�B���a!�!�0�ON�.��T��DO&V�ZE �����"���`�����j�a���S��
)���iM����RY[_�+
��B��/Cd�,�5�p��]Q<'B��k7z����aa��������0v�-�$�h��!�m��D(i��XYU��#�k�����7�	vCm:h�H&�/�J[;�����ZB_sHk`���0���r�Y(~|���9<*���[r���k��.������������'�����(W��Ko��VR�.����<!Y?"�C ��
��(���'��]�6�]q���Sk���3 77���_�/�WJ��*j>b8�>C���M���o���?���}I����k��l6��������^����B@!����9J�8��/CF���XIoEY�� �y�1�+�a��_?A�=��#%��A0�i�&A�=�����Gz���EL��[�
�T�������~[p ��i��];1��uQ>f�1���Ni�q�&L�@�������Z�'�b�����/�����z�-[��D�Y	�r��E��1Z
�������s�Nw�����������T�X?��!����?/�L�:�@(>��b,���n�-�^�h7x�`q�$,D%�xxrb��*�	@�rB�?�,��c����T��?�!X��]��W����1�,�������C���2��B@!��b��@kz�@{�2��`e������*s�;V(z�5U�.��+2�q�����UW�}73	��A-[���I�T�ke���}!���o�r���Wf>����y]�D�hUO3
��^�T����<�u���ds����]W����q�W�m����A�Hqv%wl�	���L7�u���9�u��sFnW��<������X�cz6�|�'n]�;7��#��.~�������,����f���
/�DG�+UP�]�[n�_]M��:��q����';�:�Ds�^M�>~�"�G�'��F
e�U(
�2"�0em��*5t�$�p��0Z��
4�B��������o�'�|"�|�w���.�H�A�u�w@h�A��@��P�!F �Sx(�!�#@Pu���jZ������kBx8~���4h� � ���^y��B����G�������u�	��������>��#�[�
�������/HQ��0�6���$,�k3����t������vT��	@��B��2������uk*�`EGG�'qQ������ gn���o����$�[�@!�P(.�M������t��J�l?����u��/�Av��_���X����;��C����J����W����)5+�Fq���h��;J�HU���V�3�����Vj~����������,������6����:��cv�
5����������s��������)�	����U��?����4�\NmV�:>)��=��o�=YQ���c�;�L��_h�/�.����I����[1����q��My(��o�����mGQ��(�~�7��Q���=�jt��tlyc*Y

)�wO���?�d���yd�g�A+��Xv�}tn�Y��m��+
~g��P�v�}���5�4o�_��
��b�pYMm
��B��!�����.��R���[�*j<�!s�4���_~�P���:��$��2$B�e�7V�����W��1�a��@�^i���8
���7
��$�P���s	\�����U���}�D����Fi � ����`�Az*;?�HbZ��G�?\t��&$$h��qF,�������% 2�@��2��B@!��z�597�=������h���A�;���wl��S��OW�5iB��a��?�@�N�r��	;v����4���{��1��`����!��b�L��<�ET'�E��������\=Y�'Lia'�����\}�Y#,t���ox����s���SS)|`�p�	�Bu���������v#m�`�N��x�����ed;|.El�� co����v@��w%����SF�9J�g�+�q7��V1���i{$Z0�"�M�!�;�"\z�k�>�0\������fy�e�?_t�J[��Rf����_F�wq���������������#������z�1�}o���������C��n�<Da���O�c��B@!P�h6d !�������%"nT|+j��������:��������j�;��C��@�A�����Ik�R\�l�("QkM9�\c�b�-Y�D$���1^ �@D��p����!�B[�`��8������p7��
�=�a}�/�cz�c(e�?�K���\[��`�~ �Rv~�DH���/;^���,��v��"p��(�P"I�F��)
�������l����j�
3����h{������a���F�a� Y7?J�Pp�p����	���
5��la1M���6[2�G"��?���v�QF�0/������4�������L�	WWx&�n�c�%���$`��F*�p���@c��w�g	f���oB�4g��{nt�N`���}7�p��{=�}���Y�
��,t���@�n�bju,D�|�����pAn�L..��G�{�N�����/=6�)?5���������~#m�����q��q��
�����v����?n���eq�������c�+Ak7mB��}�b���h�

����@�!�	/��|�k��gb%$�@2���{O����4d��
���q�8�edd����P�!���!Y2�B��K����e��P!I���e��|AJ��m%�m������"}?g�����U::v���XZP��5P,�V�Pv~�Y����
\+�-�rp��V�P(U�P����7)Q���Z[{�(�����D�&����/]����GUZ��`���ZP����x��y6�'�U�z6���@�6��to�o'���(:�e~Zc�@(��^��u���c����{�����Kq-V��1�B�J1W��H���m�\��9���<���<�u��L�k8e�s&����W/d�/������RBjm�� $�����@n��(���mF���VR5q>���W���V����V���O����m?X�����b�m��}��&�x=�����&��%_}JP*S(
��Eq�RRR�Y�:���R��@|$=Q�������	��@�IE\i��lh�X���<�Z�J@������o ������g��![����H65��P�!w�Bm�X��w����m��{���
pQ�&AB(������p����l����|�1,�=��>|���TqE#����3\��)
�@�"��n�?��A�X�2�[w&���~�o��{��������~h\)�������${P��W���������2��g7��Bih�x��&LU�kU���M[h+�u+��M����Pv��H[}x�[�yEyT�Vv}
��Cc���k���$�P� �@�/rp/63wW�B�p�����?$�V�;�J��u������M������A�������/��8g@R�o�s80�]�iV/&12h�W�tvk�b�K��\<$�����?K�����}U�����GfI�Y9��������EH����5�p��n]���B��`��q���q��*�o��*
��B�<I%��"�/�Vg��%��is<�������,����jv�����i�D|���zJ������:c�L���sS�Cx/��!��.s�*�7������<�<y2���<�L�"\�����3��m��&��,�G��A@���H�����X�W!�0�`@�H�"�����#P��`�9c�����*� .0d���0��%1��:.hou�/K��P(�f~
y�w����\��L�m����Da���UuE2�����TO��gyH/�XF-8sm����e�v���X�T�D�N���c�+#G�C��RQ	OP}�Y��xB� #81��}�.�w��]����q� ��_T�7�|�N���o>'��iyEL,)=�����,A���0nn5�4�dU'��mb�v)6
�������RX5gf�j`7��B���5��?�c�B���{r�\N��5����A��Q9!��c���]�W����p$@�7r��!��7`��ik�C�#_��������qP���_�r�e�8�����O����� !^�
��B�r@�d�E�=y �����o�q�Y�	��cG�����1����w��x����w��r�
e~��� ��K��1�2M�O�4Id�Er(��)�F�M?��#���L-��r|�7o�\(���Q�!�0x���>��  ����������S�����#F^���y��������a- �BD~������} X�"��xo�%W	hB�R��J"���`���������%��/&�����B@!�P�!���&�,����B��_u�0=�h!�5z�[��:�����"�F�F1��������0����0���<~�t8#�D����������K��w-�u2���i%?5�iQ��|#zs��4��"G�
���mk�2+l=��+�?����I����T�}�
�6���f��r��f{r�c2���l�z}qM1�r�V�8{��DC\n���+p���cw��z�6��~&��z�
��N����������^���s�pJ�����qZ��up��a������yu<dt'����r�o2R�1#n���j��0�{7r�b;��%���"g�]��8��)�B�k��!��{�r9Hz��]�Y�N�����_3uN!�P(�0����u�cCG��T-�&�
��kP��X���C�����y���yyy4a�'��,�pc�v�UW^YYY����0a%�:���\��J�����C=$�|��g��caL��,O@��!� !�����N���E��c���
�%Xo���7���~AR��C}o��J���H�j�a�����~%"��D������_��,�2��B@!����ad�'bZ�O�LV�M�5�6�8.H�m�t)�I����`����f�u@����p��CXx:�zC�;f�N]8cXM4d-����X�u�:��%V���4kJ��p����3���'(�>���@7�����!�Ncj���A�9+��i��?��>����C�=-�'�����$Qw�Y�0����A�l����{���t42������2�"#1Q����- ���!����0��c,6�8p��[Eg{��:3�]�]�k�x]��^w@0���?�6�HC�����^��*,��T������*��?���N���?� �)
�@�G@��W�"�Yz�u�-[F��s!�Z ���O>)H5����xy3u��?��|�x�
z��������H�ZY������
�,��^�� v��/��;v���pVv��zW��� }U�P(�@<�Sz���W,s��b�T~�XQ��p7��vO.
��b�}r�������CCL����0�g�`��z�33����z��X�_�^�7�c������;���}���'6��z�	����W��R�y�.��nf�e��D+rK��=�'b�*3���~����}�����6�����'����	���"�/`�0$��JC�i�{q���C���Y��'�!�a����Sh1��6��e�V�uy�����m��Y~��kQ��k��k7���T;
��B@!'N�|��
�P����2n@������;�%���o.EU�	8&�i������93�J�����$$���gd�AF�J�w��o��
��B n��![���s��fql�����q�QvE8���Cd0��@��:��p��7g���J�[���!�`M3$��V��oM���zB�F���������w���]��A���J���$��*����&�(�*��n�d�{[�

	|dyka7d+�d�I��q��]��!�t��a����t"��(����omU��yU%j����V:k��,j�����%�q���x�!�&��U����c��tQ���'3�b���3"�1�q`uiE��?(�u)�qLRe
��B@!p� 0h� ��J�3fW^p�7�tAV�����5��Uz�5�p5u�U}]]�p	F�Gd��f��>\��F�e
��B@!P:ZF��Y�L,]� [�|��4�����������K�����G�tM6P2�����c����X�)�ai�%KM������8������2�.�{���-fJb�����Z�K���q'�.������t�&��"kG��	@o����
��rx�f5����L���q��l0PSv>W�F��v�����>�u��E&����f2c����B���z��(��R�1.�OmN4��}��b;E�������T�$ ��������%�%��l��F��P(�}�D�b"iGe�T�X�k�x�?,�f���+�^\��K�&��#�diU�^	@��^�b�s�d���"0��������[��?�v};u�P(����pZy���w���X��`W\��M��O���o�k4�\N
SD��}�&��U�R[F����}�=�����GYY��c�7W��\��� !���r�mTG�����yk_�c��9�2�f�;��������[�2�e�
���LV�G��L��T����YeE���K�4iF(�E��hc����N��+D�MQ�C!��3��x}z}1���
Y��D�0Li��mA�n��P&gz������y��V���g���At��;�[`��v���v�
��B@!(�/�Q�F��n���fH62w�\!�*�2�y � ���q��k�.Z�n]I���y�*�Mz��%����_	S�������~��Pv��	`�bFN��x�2]��{���*S�n �4�Mb���2�6J��y%c9�:�I�8q�9�Y�f^�L��t�BG�+
��G�+~8���t&/����O����Y ���kVS!'��O��w��G��=!�\�����g���[�
�H�$:�.���.��[����z�|�9u}�A}q�����A�����#����v�W���Fj�Q���E�6:�q���{�j#�t+� �s�n"�.����O���������D�����8�x�n�C��0�5��9+��^Vz��}c"�Gs&	�Ls�n��V�(�����7g ^��J7�7Q��2p?)�����[7���?�J[�xS��p���vb�*��������F�i���!�bc���n5��B@!�P(|#��Y3A������]�DRd��7�?C��m���6�������/�
Y�CCu7!��ou�d����J��`7�t![s���<@ T�N�h3g=� 2���3�v��-*>��#�`��>���7^={�,��9��P(��������W:�||�i#uo���qP��6(�������C����yBiX������=�u7�|k�
n�*���?���
h�2�*����o������\���R��E���fp�
gu�MsC9���g�[�@��9�7W����DW/2����ci_3l�������pf������
��	�#�l�\^�qm����k��gw���ew����d2'������;�{�p#��m�h&�`&��+q�svu!��f�#�	���C�G\L��uP�f����$�p�R� �0��Z�k���O?Ns�\M��Np�����u��B@!�P(| ��S'���/}��,��$���dP���	�nI}U����������|�����������������
RY������o�L�B�v
�h�V���0��2��u�-���	���\�������HU�P(*�C����h*eqVo���Ga,U�V�D	���i��^E�������_U��@J�yld����={h����&���8�����N�iP)�V��NP�?'&���Rd*������t6.�N���|���D��!�����g['2�+.�?���H�A4`��j9�y�3�$�������l� �P�L�����	�(�y3�b.=�J{8N�/�X�'M����Xo��9&�q^���W��E�_�t%q��|���>����/�~)e����w?�?o��v���{���K8.����VW�v���G7�cu�P(��1y9��y���j*E|w-��h��v��9G�����'�����<����7���t���5k�P���i��
4`�j���a�����Bw�uA�U�?� 8��?�XT��%�9v��8^�~=
6��LHH��n����Ap9]�l�\�R��zWj�\�|9-\�P��j��?}�t�j������u������CE2��m�
7R�g����y��3�<#������>� �����c�=F���P��_|Q��C��������|h��e���n��L�/�>�������R�=D�<��>i���>��(B]�����w��Y��}���e!��62����-Z$O9�Pm�n���j1sV��`=�O���\_|����u
�7�����8�.�z�-�BU�P(��nb����~���R8�D�Za�+�M�L-�E���s�X2h��lN���i3�:��~���r��:N��_�� I��L��?�Ny��7GO/Y�S5���5b*��������32�1��N���n�Z:��i]���n-T#P+�"�5�_|B;��GI��p�l����������eP�%^?��M�Y�O�|���&��<.����q���0(���/�/d/�M�,�{��F�;��!��	8�;Twy�qx&��1����F�Y���n
����t���N���H�dt1e��J^Q���<�:�u�]Z���I����F��Vq��_�d��|��2��0�p����>���o,}�.*�sk���g����z����^���vX���!SD��k5���I�����u�P(����b
�$�xe<�Dt`��	K��d���(�U0{�lA`=��s��_?�/���(A�dee��M���+����~Z�I���� ����a���l������JDH<�n���C�~P�c���}���u�2!��?>A$�z�j2s8�����/Ko6o�<RRk[�lD�+��"�B�8S���z3p�����W_����!�s�Nw}����Ok����d:��z�a�w�y� m�Z�,�O<�����K/��/�g�G�����%a!�^�D,��7-�8�5$!AD^������4�� ���C�y��WRRR��|��D�$NE|_q�u��: `�s��{X�����i��������=����},B�~�������Ne���r�l���B@!�P�?��[K�m�LEu�?��k���.w�)?�������U+����GO��[��~����;�D�5*�1_^��f��SB��u$��C��IOd���7�IJ�<�U:d��F�~j�F��?#��u�`���c��m���K����Y���oh�_\�a!��������?���f��[�m�;�8`�@�q�<��;��J��er����	����$�����j��aWXik{�HI)6�����h��z��>���L��"����r�o��[Y�g%�����@&eY�	d�P$�
���3��U�D�$�-�Z���=(��7WE��w����5�Q�=�������$�di����]�[�W]��:M��v�9%��^?*1������/e
��B@!Ps@�6m�uV	�Q8?��\&~�aQ'>>�^}�Uz��7��O>�@�A����D$������]8���J2�cP����[�j�P"Q�r ��J�e ��%I2k��A�)�m ���������q��w�}�aw�}7���4r�H_Cz�c��$� ��� �1�n E����I�eIXH�f����m���^	@��q�}�����n�� ��	�K� G1G28w��Q��������}� Av�0g$=��XC��k����?.����r�X� 4��������Ne����f�����m�@o��2��B@!P@\?I�i�7w�~����e#�����7f����}���
1Pd���/O�/yl��R���/>���*��b 3���r!����������7�u���Nm���/����:f����i�~�'W�u#�p�����c�������7Mv�")=��X�_����n��8�����LW.
��0N����:y!��x�>�7g_SN��.�@�������~�����~!.,����r�$��p�V�o����QO29����|=��������Yu�o��
d������+Xm�g_.��=C�u���yLR�Y���C�GEg���x��������T�D�z=���K�����PV������D����Q(
�j�1$W���KE�2��������9��7T` |$����.���Wdd�Hj�a�spq��7�k�B�a��@�^i����(�7nk���a=0(�!���5���t����7(������tw`�3PC}I��
�^Xvv6��F���Mi ������n���C?��/��,1�kF�/�Y�v�Wp?�^��~e[���u*��~�%�uj�U�B�S(
���@�WSo�����ST��T�eM#����Q���(� O3DVg���
�;�������{��ri���������"w$�S�cT��3�S�6P��2����o��,��B�2g%��MVV��N��*�e���|��������b��?(�\ �/7���L4�/f���F�[��nv��P����e����.����������&��J|0�JG����k�0QL:'����M��"AH���C9�"�^-�����P���
g��9v�x��OZ3v53���1�t�H�L�_wv�0���K����,����B�.��KQ�����A��I���"
z�u�^v�D��^NW���M}�B47�n��B@!�PT*�z-�������E���f�E���d���~����y�������M�>
�]*����$Md�\<(A��H�����B��%K��a]�v���f��v�w�\UeFXme�*4��]�>��\F�(�rOkX��:��^AM��8p@t���v�`�@?�@��������n%��X}�e ���!k1����d���>��/���,%%E���#���Q_���SY_�#���%m��$��y��B@!PEx��n����
��ln�[���W�j��b3E�����nr�J0�#Z�/`h�������7a���m������i-�T�����d
���U��,k����J��#�/2�d��a�u������>x��L�@IDAT�"b�7m������:R�����*����DI-��������g�s�.���e�����Ys��l�Wo+�g6�h#��xBy��38���!f�F�[q�������7�:�ic+�b/�(�9v�����u�o��L^B-OjX7��Zr6_������
�R�&�P��Di|�����s8�$^��$�]+-:��I&��%o����i���g��
�F�N��MbXa���
��B��(�{/�U�8{���d3g����{O(�p,-33���\(��� x�V�7���eP�!�\;���C�p5H����(�b,��o�%����0_��pIE[����C�
�bN��+���VE���pc�a,I:�
<�����Cm�R�GGG��x���4�p������:����$;QYu�)�z�~�u����qm���c��A���~=�����:�u�5#���%mu�z%UW�
��B�|!f4��7��I?n�+8�����z��_������������v�~����_W�.�L�p~
����.i�H���3A_�j��~�F�j�H
�u&��&��W/�5���J���1Wad�Mc�����4��������-4�b� �2�	)d����+f�N*�L���:a ��%��6#5�����8�cn��k�]�'	4�0<fV^YD+�Z)�	?d�=���y�����0�DI����uE���"Bb�����f���k�CR��!�W+��c�$����i���0�*�+s�96Z8�B��-����=�������O���L���@;�~�|����L!�P(�@�{��W�bp�E�4���%w�5W��,�a�� ����������7|�p�i�XtH��~1.d�j�j3��������p���JC�7��.b�i
	%3u`pQ��-p��$P:c2����K�������v G�
;�O�[���k
j<o���L�C5�5\p���Uf`�x�1��O?�f8��!b#jq��+����������T���h�Oy��V���Q�
�@A����w��$��O�����:��P�4VZ�R�./�
��GO0����V��I?e��@������_���?i���������d��)�����=I�W�n�ee� �1�n��w��M�\��\�������d�MoF=|R�����fF�> o�Fw�Fg�	7�n�OW+$j�B�XL��9�!��%s��MFj�$`�&��(���"V��JAV�0���WD?����<6��
��7t����f�JD����d%�?�9b~��j���-��������#����7^�|!��BN�����h��
@e
��B@!���H�?�P�@�����&b�A)'
	�
J?$�Vfe���l}��H(2n�8��.�	2
�Cl��	���=j@&��d,\��I ���-\}%�#��5y�d�Cb�)S�e�L0d��1c��B�b��~��=��y{��M�1�����WK~��J���;��y!������`�zG�1��������������i��!S1\��e�1
w^d� ��4E�cN���%��l� ,QGkh�r��{��� [�������G�D\��kg%����T�E? ��~c�OC0�T]��B@!��4�rz����o����?|G?��A�9�7���{gu�
���V�'�\����VY(�<��t�N���������$����Ua�&��Q�aC(L������^���@�!�\kS��E�rn-��+����O�>Ba,�cC���?O�=����(�J0�=JNE[isG(�,�.�y��j�f�;~	�K�i�
u�$P��yZ�.t1�&�9>���h%�#�nZC�
y]Ns41���$��0��fl���"�� ��4d�=��FH+���D���V�e.���S	�F���U�Pj��B@!��� �2�"\|,�U����� ����A�������'�U�SO=E��=����BEwR���#��t�"\~,X@���w�O�4���d��p�f�G�q��L�A9�� �@!N�o��&O���o��&����� .A�a�-t���"v��#���O(�����_�� �v�D e?�`����!�/�J0�B�������_|���_px��A�Ba�d/W]u�x���"�/2+��D�C���^hc��$�����{�8����/���3��G$f��P�)�]����~��|�eyI��a�nKjR���C���T�YV����~��k��l���_+��P�+�A���|?[��L�9������w~�ev��n�Y���vo��u�����I���m�����s���M���?�t:vr9.�3,���1	�*A��Ce?�M����^E�cW�N�������=y5���t���v���i���XL����5������+p��"��f�`�U{u�
4~������H��d��z������Y�t�G����p��]��@=�8�1�5YQ�����X��DU��}��F��������"��1r� �����U�Y�Z
�.���/����~����z����M��X�3�
���B�y��������x\�:5��iAa����
����K���N�*u?X���vD�P�{I�U�>U��A��6Y��T�^�u+�6������@�@lkG�d�$��
<d�MNN�����5�}��.\k�!��~��g�������#���JC�f�f�
2
��C=�Qq���>Q���s !1�V�&�c{�c�#K�t���f�a�6�������b��1b��~����e*���N�,��1_���m�BT_���{#�e]�����X��\��@�BU(c+j�r��>���;a��Y������V����)
�@�#0m�*���~9^�"�0�f�%hB
�����I9n`�&M��7=Z����:pK��3��Hv����K��� ��5d���]��];��k�6���U�����������P�S'Z�@S3��mr�V���}��;f��V��P*���|#��a������������tC��8�^`�I����������E����b��BMO�jc�}�V��x{�����F4S1_���C6):+�6v����u	`-���B��d]8����hg;�=		�1(]��;����Pj��:�y��T�A4����s�;.&�p�E�;g/A����I��i$$ 	+����n�O����B@!�P(� PR��dz��,[�����B�?��@�����.�gP������E��>���7���q!�i��������u�~�8)`�8-�}ae������'�s�[�6mj���			%��&=�U����.��P"c��i�|U���S����`�?�u��P�L!�P(�;����F�!C/b�E�B��		�td��p��{Q�������-'OPt���+�@������$B��{vS-��t�c���X#����5c����K]b���S39�pQ.�i��	[�U\pC���I9�H5�N`1y�A$�Q��cw���:�b8�jC6����	�=m]W��k��[��joX��P�c
���<#T������c3��Dv+{Y����+���)?�
���W��:�,�K����V���)���e�~��s��?��JgY4��t]��U�(dC��0s�'g�z�)Qvr�ZZ�������l�����dW����20���B�,�5���:�{7e��K���������h1�eE�kK��\Fg�R���x�]��|?e��j�P(�!P�^`=WL��'�L����i��CF�`H���n�����;����o��4]T�6p�E\>��*��r�����.�>���B�l�P���X��%�>���`�Ru
�@%!�qE�hb�u�iL���V4���brp%�-���qDV��t����������i�%
j�?c�������3����m������� r�C�	n�u�8�B�%e�~AT�����:� ���;��F��X$2�F���r��'�zP��aR+#�~��p}7wV+� ���D ��+���[i�%����Hd���'hiY��C�������~ves�!����8�J>V/��������#M@����#]�i�/�)�X�!H����L��@V�g9�]HV>���,Q�}F���h
��$MN����8-�(�����Xi�
���`N����8�E�Q�N���F]Jx��?�0�=&�D�[�2��B@!�P��hC�4o�?�/WH���J���qc�MW�����{7c�/�TC=��.t����?�(��7Pk�\�p����/E�Jm�@�(R��B@!�� �z���u��h*5d���������]���'�L��rj�H]�����2����*�r}]}�`-������#���Nbtz�o
?e5d8�+G�bx�v�H*���OQ�>�����Uo�So.0�i
��j]��D����ma����<�`Od�����k�/��b��W�D���fVAveql@����bh�0������V���c��uW���-fw��My�L�I�������������|
zA��6�x��Z���\����(u��c�R))	��������xd�z#5���!��L��8����\u��k5v4�y����U��)2�E�C��
��B@!���b�!QG ����q��ou���[�
m����n��:M�B����<���H�	I���"�EL�W(�������b&������V�s��������.���gN�^�'������C5�DY�B��=�Z3���*eVE�B�*�����G[�.�F5�b?��1��T��
YE��y�����_�n�.u�%X�T�����o�E�D=������x��g�`��o��WX��9t��=�@V��-l��	�q�0��x����.R��<�5�Af3A��xs�p�2�<��P�;���*Z�OE�qv��Q#)u�"�\l���v�
��B@!�P(*EV.�j4��B@!��@��C������`k�����[��E�����$�^Z�����,���{_PV>9x� �*s2��5�PC(2(�o\?��� �,�OJNO��mFQ\���11����+�����Ps+�����=�����lH�!�o���1���R9I���"��cI<�������8���R��V���	70E�\�;Q+�p�:f`7a�7AC���z��//�c��M����RD�? }�T����|��-k�����Z����V4.��l��k��t�@mu��P(
��B@!P�(��W�)
=]����s��L��",V��s����'�S��M99��<��L�����Y���2����N�5�����fq�,�V]*�s���#<�����U|a�����|���8��-�&���Ut�
3�f�Ts���%r�
#P�{X)���hV��L�e�e$5��8����#�����9��{g0'�]`��}{us�x�@#W#�FYX��u-�g'���q2����ou��p��3��f��@�_f3������W���Y�7�c�����lw������_�Pv������J[e
��B@!�P(�E�?���
��B@ ���a���I2s��
�K��tCf����g����s�pv���|s�������#���3����`
�M�$#��U=J��,�u���TTV��gYi[��R��.9����w���p��s\�Y��3�`��E����n��F�i�����Q
+W�u�h��J����|W�W�;���pS+��Q3N�sN'�s�����nmo_$���Ow�op&,1r����������W�i"�0����Q��*�u��&����/�AX��EY�d7Ao#���v��d-*��ov#��Y���h�O���j�P(
��B@!P1��6�b&�zU(*��4{�D�����E��o�w����p�6��_h�����5���1ci��3~��w`?��WF�K���A=Y�����U���������}����F�{����EY��d�����.���P��K�b$3��7d��(�,����N��)��d7�7������1��6�8�_�����"����p���Z�[n
��92��~?�+X�g3����u�"#�de��6u���D+�����y���l�[�y��� ��l���8q���D��{8�osN�������hS/�F/^�2=?�1��H�gC8�1����h��~1����l�u��sG����FUV(
��B@!�8�n9��D��
��B@!�B��b��������~�r	IE���������mGE�~�I�~�7��.3�9��M�u�D�$&R�zQ%���U��=s-����������G'��{�~�4����+4c"c��>i
�������6���0sz.LA�ueE��^��	o�����Lpw���9���X�O��=$I�c�`)��������I�lc�5�9��#��/�����"!\|m�����d���>f�����%DFc��P0�G�'����:;C(���$ {�����csS����f��}m������T/1��)U�P(
��B@!PP`|S��
�������y�������2,���[�����l�wQ���*�������mUce�
��(2���3N0��!z`�=�W�Ga�0�9yg��-^��?��M��Q���?C���f����7�L��
��1VZ���wL�q�����h�B�8�LP���4�O5�9�?����9;qC��N7��������W�������-�B���}a~�6t���h����vO)�I3�� ���+�e���87�,�7$�9^ }�:
��B@!�P(���*S(
���@������������Gu.���
m/����6���e�[���5���>\0hZ���Y�����+ON�yed0	^�Ior���O��g��*�m����BK���"�G$�	�5�DZ~-���66A�Irme/3�p�����vk�x�W,u=_m�"b��AD��X~��q6&9�o-c	w��P� -���-��&������t��DX����Vb���R��af�L��#�����Z9K�U(
��B@!����Pe��*
��yG�_���I�x�8��+�����]~T�F��^���}�e�t_ K��jE�Z��$'����o����i�_�O3n�M�u��a��k*	�@Fp�D���zu:5�J3�d���J?i&���VW��C�L9�LFQ���7Z�!f<����������Zr-���_�=WK��A��q�hDt�S���q�v����q
�l���E,?�����J31		��/����������V����z�W��cz����1����i�m�[Tp6]�6N�������c��B@!�P(
�*��"������P\��0#0}�8���F�g�J���EV-���0��kW��	����PL��T���W��{fRva�s��������������&w�2���$&����~��tS�m���bW���]�'�pW���nE�g��jsw�a^�����2FM���YI��h��b8AF��F:e%����������  �JeX�<~-l�q�\2�~;L�o����n(���,�Q|�f��ns
�j���������h��f�������+qZ#]�n��g��B���w@t\�����	��:-���t�(^-.��r�#cx-:�t���s��sP
�F��2��B@!�P(
�j���]s�����B@!���t���h&���q��B�����6��RZ�V=70��EVg||����:t� dV�p1�2��*�LjG(��|>�
���c��S��%�{�)��gDs��ir��SP���	����]�6��UD��p�
�@��� A�y3$�j��!NZ��DF&�n�2k/2�B�8j��6fXh�a���u���8�j��|j�����T�Y�k�Q����K�L��\�xy��

��B@!�P(������R�T(5��bj�Z�����u�������kT�]�n�p)�I�auiJ���:t���G�i�Z������`�|����,��A�<y)�J���Re�}��3-9��+�����;�u
aL�c������i�9��p6J�b��@�n��B�v0(M����,����E2�9�/��	����C����-�j����T�	?IF��v�g���fRaF`����8�Wo��L!�P(
��B�!��5��TKQ(j&F�����R����I	�<�.IL�z�Y�W�w���z������3������S����E�y����'���UE��yg�����WR�'Wl-"��kg���u�>1�MlT�;Mv�8���aV&Q��:��Ix�j�162p����h�x~�������%��P!{�.���������:�������x�����8}��� ��2��B@!�P(
����"k���V�PTS�8�GdX���zQ}U�e=�����~�K���Y������)����!��G���;y�UTA^�):�mE�lA���&53��y\�M|d���Q�]�(��hhti
���eq��.�4p��V.�xI��k������
����*m�L���0P����A��Y����5�0�DQ�$�/-��k���RN�W�^�#��R�f�]��TWE
��B@!�P(��]q5]���B@!��Ip��{g�����RL�:���a����/T+-�WS�����
����Pc�|�g<DL��l���]~��M�����E�����Z
��^w�����9���g����b���?����Bpy�����95�x�
2B��+�dv,c+5N�R�T{��iA�������
�~���:WL@��:gcw]�5�w��F6a3�d5 *�-���/� �_�&����/�^L��tz�f�j��us;V
��B@!�P(5����P3�S�R(��������O������j�bM���V���8�qM4=��?��fo�A(�*��a�����Pk�|�I����O���1�����v�{����i�����;�����_��{�v�g9{A2	3a�
����t����}��_iK��-�PF��$!����8{9�����}�HG����l������s����?�%]��9d�O�6���+6��3���r~����^*%�z@O�}��W�6��i�$���F��"���W�5�4��Br���_��<���9�OD�����=�Q�GR��T��z�����,;>XhG�8g����p~�5��N�B����WKC���B@!�	�w����	! z�[�=�m%/h��3g!����V�:�_���l��=��`������$�����!���^p�����V%�p~�x<s�sP�M��DZ[s36�\������Xy�j%��GW�9��8�@��#�Y�����?.l]�F���,���������9���_"��&[�ODm52��m��������#N�1���^nEs��yQ�K�
���g�U:�f�NV��)��_I��W#�B@! ����@'>��GrMB@���X_=�o�}�&��o/.~�9�!C��6��,�]sE�U��H����,��3���I�n�$�Kak���T7W�q��*\:�6�6n�d.(��������I=�������$�:>���_�L�����Za��
�Zs���3� �E��\��u�� ���wN
�[��<�\���P���zQ�{��:���t'�q��-����$�����_��Q�J[�?>��x�U��
09�3���`��B�u�x�eG! ��B��������B�L!���i�Z�X(>��{w����]}������|X�u�d�x���j�����q'�T��a�C�����#s�N��|��~�bQO����6��6x���R��6�
�+�h`Y��6}��X��Q�s\���z�FU��{�dl����p&����>��u��a%��X�L�Z��Ylg��h �~��r��a�;��G/{����K-�N�l�c�~�����?Cl����b���R31o�}�;��?���?��bE��Ao���n�T	! ��B���O}����	!������%	TX�����TOK;�V�sul������Y�{l�'6��<��m�_1�Bd���E�N�gD���\���-m��n��+;_���ixP8��SC}�p�O����k��+����8,�q�A�u�
�H{�^Rg(�_e���w��SO7��$�x
1N�Y�+l����;��4Q��=c�-	�����4	}w�C���e�z����������(!�m% 1�]��������q�sp����l�$6WT����d�Uc�|q�-AB@! ���L"F�}�U	!��	�LuYEi/���C����?lXGVOn_DOm��1��g&k�7����))=z��
E>���V4Z}�z��+"^G�t�Z���V���0�m��o]���m����
x�~����&�79��__��������J��	��NG_K���z���f���8���C\���^q+|�����j]���)�	����,cH�k�aqS�E�G(�o}�S�����2���+r��Z����Y�l�%���.{G����lF��)���!.+�M��t��o��R!��B@!0`�8`n�\��I@O�9�JJ4)���\}��"i|F&'�od���q���'>�K�s�]��&��__Q�<����k�A�1
���"�����7�7|�����#��see}&�+n�SGz k��:4�u��q�n�Fr���?�����?3`%�8�� �����:���\_�/��wBM��,�t�s��= ���&8�]t�O�@	=��J�����!�e�ZW^>�eiN������;���|���eD&e�j������?��a��B@! �@?' `?��ryB@D�`In�<����8�	�V2{�G���j�
C.�wL���)�_o�����Y����.���A �.�nJ_�)�^������au��w�Oo~��-�CvB6v��9�gn���t�����#K<�����G����;!�H��,���J�
v��2���;�m�N��\cW%9����$����RL�8}�:���;�2!����N�.�`P��(�_Y
�m��C4�� �����6��KIWU\��6�@8���H���5d����Q�����_! ��B`p�d�D! "�@nR6~�^l:]�o�\����8��w���^����m��������`Jr'�����5'����{�������9	��:�����I�����g��P�\y����$�q�\5~�6s��,��;m��j���	:�S��x����Y�b�;���c��}dT��!����t��S�#���18d�����tI��r���].�{F�Q�+t}�R-yM�d�j��'a���Fd����B��S��m��6T�����J��rz\��a��B@! ���$ ����r�B@t#��Zx�9~�<��+�����99:�\���&��3��S(�j�P�����q���NL3���n}����z��������*8�#�k������q�8{����n
Y�f;�f��,O� ��d��?�2��;���;4���t��6a%a��@���8�Tv1fKD�����A�eu�vZAo�^����)�,��3��-Y+r�����3�L���������u��B@! �@�& `���z! �8���%������U��������G��"���_��-�.�������������7Q��&�1C�������V{h�i
����rE4jS)����Iv\���X���q��{����	8X����473y.���O_�ea/��|r�7���|�����hG�i�2l�u`�$����n�����y��|��v��L���RO#�f�����<f6��g�.�� p�_����<meG! ��B@t������� }��B��@`�'p���2��}����_���CC�#��{��j%I���/�u|yr=��
9���d���d���hmM,��Q�jb��:k]j�KxS���)���5�>��q��;k���N�&8���x���^#%	9=�u<�Xp�<n��!��c�����WE�"��J^����d�x;f�6�D��I
�S��Dzl�f�UkxE�_�V����kY���0�����V]{#�$��
��E��7�����E/��^z�E�v��H����B@! ��B�+D�
=�+���"��%Ztq���I=ZldFE�Dq��z�U�;lX���/O���l��t~%��.)������_P��as��	@*)n��/���
��T�[w��F����EZ���Xa��z��m����6����6J��?J��Sa��T���$�%��G�\%���z�H,�r`���[��t����j����9vQ�t�
�$�q��D�;�e�(��aW,[H�Q@����
��;�Gc���F>�Z�)
��S���n����u��U�n�>���1���=�}! ��B@!�e"v� ���%�H��[��YG/\{=������A��gv%P��U�]M��������@�jhm�W~�����-��o����18�o.��*�P��'9S{�����h,o��+�W�� ���������X�������	�/�*�ym�D��F��{z�pC,%!����4�|c<M<;\H�%=MKRH�#o�>	+u�;�
Y�d@��JS���%��u`r�6�,yP��O�z&
����
q�H�B@! ���9"�k�I!��Px
M+���F����J��������'v���/]h��5���;�O?z��������8��rr��u��
/!/m������xn��q������p)k���(!H�"ny�wXh�"�[��g�\���T4��j���9IG#�S'��Lq��~|�?��E�`�J:����Al�GI<�'8[pu�{�C��)�`i�'�Z�-��;(s��.Y��c�	=6L��<}a�OFq��W-�2�2xU��^>d��i��I:���Z���B@! �@��Gq�dB@�t5��O?�(@+���������2����\���7��6"�,K�u��@�jgv�}w����\�5a9Y����PE?��b���:��jc�q�����g{:����]�rm���P��~���9�`�{�q�������Z,��\�-:%����@�,�y����J�a�O�*�J�����P��V��|r���dTkg:��qy�D;F�r��<���FD����|{���	�9N���chPB����6���`�'�d)B@! ��B�	h?z��2dj! �@�!� ��[6c���������Q��L�\2�����H�;������:���O�7s�`<z��ZU�\�������j����~fk�lO�k��]��d'���y�mX�����mNl�y[������O�������������q�,���v:���S��I�3�Zp=Y�U�xII<X���u�i��������*��������~���:���d���]rY+�y�*�)���������h5y�S�U���aE:��C+�i�y�c�o�-]��%W=-'��B@! �@O��H�<B@���/��IB�Ky��x���g�>Y�%R�����v�u�07`��'N��^��{~���g=�J���������2�0i���NH,�PSy�� ���c��-�:S�������D�����lO^}���������fzT��v���v�
���v�����dIpz$4��1�t��O����}nT{DQ]]�|N-���^�W\�N+�i�������j���Z�n���Al� ���|��w�K)��! ��B@�^&��o��h�^!���VUz�?^���:���1z�D���z�b��q��q����V�f����q�����@B�����#����zv���c�]a�@�|l��������c�u�A�����y�:8��K���G��*�
���������E�X��=��dex�g�
#[�-����O���sqdx�	9
��wH�w���������Z��q����1���#*Ec��m �B@! ��B�	����ej! �&�srcc�iX.���fN�~9\]�~�:;4)?�7?�V���[�y���UMU����P�R�-�1222�3�YF�i�����>�6n�9V�k�U���V�����j��oU%�O�K�Md������}j�
�)a!�-j�E����+�ZQ4x�"n���{�	GX�������~�d���<�c�i<��Ym�e��^{��"�i���B@! �@$0����E�>A��i�q�����0&-�X�PY�A����1D����V�^������.��'�CG�jX����Zp��]q�,���X�������/uz�����F�-�)��u���Rl�b����
�����.&)k���/���T�����.��%��g���.8q6r	�Nt�� 7g�h;�M���]����N��c��"��<���
w��<��7�uh2�m��*�����q����vJ���B@! �@����R{�,@��L�@B�-[��e�F|{�r\1f~��3�����y}��JM���bf_\�v��*������"\��w�����o�����'�$�*X��F\��mg+���o��m��-G�q��`6FQfhor�fk�8�,(+�n�X���l�V9I�}c������S�IJ����|�����[c��u�.=e&����x����XK�O����h��|�"����}x����L�$����d!���Ok��� >����O[���#��?.ZaOm�u��m���;��J�5��u�p��-C��GlN6��9Y����@�B@! ��H@��)�$! ��@M3	%�����	8>���y-6WL�-Eg<c�Pf��X�S���R���1�[�{_��y���S�f+<������qi})�i	l��1�X���4,n(�B9�]��M������179O]�n}y��S�N���N���o%� �N�������W\��'�$��Fu��8��e�m��(a���9�R���^�C�i=c�0Z������E+�q�����������Hs���c�F�Q��}�9����1�����~@m*[! ��B@!����#v��0! �@�	�u��Xy�0����9sp��)]�xu
RcbPT_��q"�s�����F������L�W�e�Etl������V�;lf���V�r�O��xlqJ������+�������q�2wV�Sbq,��7������f���}7h�S�������-z�������IsL����0��jP�|G$�qV�����u��X���u*�����!K`�Q�:! ��B@�H$�m$.S�$���<�
�N����<�{��ue������N���}(kl�t����FW�
���\7~�<�7��#s������M�Z~��](��6��@o�!�:����C������w<�]��-������u�����sK����^���Vv�g���5cq���vL9���f9�D����P]z���������6T����N��j�������9�@���s�h��}! ��B@!�D���"�B �*����T�;p'��cG��n���*Q��������#}���������6�����:�x�+H���g)$���U�V�Wi���������A��xG�q�J;$N6Zz^x�;���M����4G�K/�+��<+�9�� Pm�l99Hv�NI�� �>o��M���>�!Wce�%U�N��v��v��
[(�r��K:��+X����	��
P�B@! ���D�wI�(�@�L#k?�pk%��KmK�fewz�=%%m���,,Fra���+���#"y�>k����=�`Sc������<��&��E���3����g������^����)��MV��b��p{Bf��[>K@IDAT�t�����s��BI?�K	e��;�C-_�V��k�Y\��Q���r�
�)o��6ma��1�j�qh�y=Utyl��Z5����~����qbr]����'nf���
! ��B@�~E@�~u;�b��D`Hb���<��d����i�Gq�:SN���?�v�t��c�����7+B���#����T���Z�X��7X`�kL�h�)�S�JIc�����������eDJ������FA�S��kk����V�w.��TIl�Z�z+���kku����|�Y�7z�F��
�*R�$.Y�
��w��������j! ��B@!yD��{"+B�MJ�c�/���U�J|�����R��.���)������x��+�5d��33w66����w, ��Z:5���<������;O-<�;����lkuP�>X��m��?-+��8:�3���Nq|�3���G����@��������7P��V�}jg�����H���@+}�:sp�V���9�:4�4B@��K�Py:����I?
:��r��#W&�@�0����G��$��-�����}8"��(�(�a�b��9.#��l�9�~�2�4c����L�]��N-~p��4�V���yO�s�K�]����I@t '��`�������{c�:�v�y����-���1�H�x
Y���\F��S�����W�E�v�U����u ���,Pm����[/��$�q=/=��}.��&���c'����R'OTO�V! 2z7oG���a>���'�?.��4Rp\������7@�TAu��S����>�3��~&��\�]�"��;)�!(��V���h��1Z!|@��9�Q���b�7%3�,��Df||� ��H,�}���:<���������3���/|xh8Y����'�O����P���4�b��X��q�"��|�Nq���0��I��X�:$�����F����:��L+0`�;��_�~����:%�`l��o{��@rY'R�_�Z��x��Ez�������N�X��������=�dG! �
&����R��{+t�hSl�W��W?���T! z���=EZ�B �X�y�����������%��h���������jw(��"]d��99xf�50������x|��p����l1�������� �������<����x�j���Z|yj����OBf&���('*)qGJ�Dn����l��'��~�5b������������u�n�h��*N��������p�U�S�P&���U�S����j�vk����K/Q�z�B@�B��
��/\��������\](��#]�-]��]' `��B@��w��'�����W����������8�G���1i�>	2���K���
����������O\y%����9"qp�����_�I���
�#%&����m{�;r����uh3o+}y�i�A]+)g}�\�s(�l�+.��%�}\C�u0�KP���u��j�x}1���Hb�)���Z����6���x�=[	$����v�����/.�Z�/��td�o>�QGV�|�3r�%��0v	�eN
�H2�B��D�<;�)�@/h�Z<�/��p*b^���3e~f�6�I�3����.�K��Q��F<�}�R�X������Y9�5}��[�P�����;�����n���T��_��S�;�)^���/����6���@4���hgt�?�^�*�q'�x�J��-h��J��a"�������8��Q����C��6q���m�cl,f��W�NK�B@�#��ZD��%�������#&��|�����7|��uX���f�����3�5�"4O��Q�KB�/�/�5Y�
���G�������_
z�RRB�s��Om���l$>��c��o~CB`���1���CUU�eb2e�������@+,��+��fMr
���!���B�q���������3[����������s��k_��Sk��N�$.����y$��A����E�P@�TYMu����yE34Q�
+e�M������&�7�B����"��Oh������M�zu�=���m�33�RV�
���
7�6/B@�@@�TN��#�����K��+�/Y8����:�Rz,�5�\�dW�4���)�>!�B@�^ �o@�|P������"����{5������6<�s;��&����`2��V�����;YS�e���*�7g[qnz�5$������TXo��eW �\��u9�d&8��*�V��k���_�SJ��DY�X��l���^����~M�B���S(���$"������z>�������)�\6������ud'�(��_�o�>r�y�V,_�>p�J"��(�'7aUp�-_+���vr�Mj����F�����u+�#��u��'�����k�����[�[���A��L�}Gs
~��9�G��8��3���6wRX�����N�'�\������=M@�UO��|�����]z��7�X{R.l��u������4�G(��~����;U(S����u�"XF][�Tz�s�Wq��~]�k�3���]%��rWG����4zZ�t'b#�(��;	�ol��g"k5~�2i�`<J�����l
�E��l)x��~5$��#�J��a�����i����|yj�"���,%D'`������w��M/�y=MF1IY,�d��s�V50��2����f����J��pDZ�:�G��������g������*�p�OT�_�S���<Jf�zwb��#��w1%�/V�R�{�Z��xg1:Q����2�Q�� ���Fl�lW��b^��~E+��)��0���w���$�w�����;z|��o��g�{V0���<�
O�������maA�#������I�ue�/
��-��2�{]�q�>s�b=�?�Xf|�K~�)�<�������xp9A�u������+`��#s����8���3�#�_=-��N�B``���^'��[MMnS�^_M�X�1	��q/Y�hmmU}c�=�J���������8�_G�T-,y�K����\��&�u��6!*�$����p�e���!��*[��|O�=��s��>���==��	����o|��h���G}+��)o�y��O�i�/�V��>��e�/^iE	z�����K����$��;���\@Xh�Hu�D��T�����5��#d�B�|������Tv�<k���?���������!��F��Sz�b�|��\7,c@�_z�3F�y
���;�*�<Z���j
�~�M����}�Y~G0o����#!��fmXc�~��/��*����ZY)�[���Z{�o�0�d������EjIJJ���������~!v������[��64`���}�*9�������Z��n�%��^|	����u��"��� ^�}d�e�XZ������F}Y�E,<+%uE�N)�
���s]����|#SG�����~�a���$�x��/d���\�.�f���P�DJ�����'������V��W�1��D����k����f�f��\n���@^��,�����9U��k/M=����>��� �d_�C~nL��w::��B@h:eq�����.����N�r����f����xc�N����b�����0�9����s�"���\���eH��FB@�.�����K�����Z����(�n�IW������di����g�?;w #6���1��+�K������o���+
>�[����tP�Y�Y�r{�w����U����*�����A���
dH��y�$4��n�
$&���}9���I��|��N���J&_v�l���O��G��A��&h�OS�����BE�	! �@0���/���l/>}���1X���S".���$�}H��B�I	�,C��D��K����W!��FB@������z�"��
���������jc��nb;��z��,\9v�"��4_�q����)���C����d��c�1����E�5��T9�RA-&#���`a����m��-��A�����a�U�0��T�����);��US�?:�V�]��Q6�u_O�R��������+0g�Bm��! �@'�5�8��c�
[�D4O�K[��}������K�<�}}KuHc:
Q��K�{/Ck�EpR)B@��H@��x����� p���O��RR����
�;�}�x��7���a��m��w������u)��yC)���H�De}����0'^XbE���P���'2Id�/�fJO��)#���j�)Y���L���k�����
��k��I8������@{���_���|Xs�t����:��"��O|
��!���8X�/pY�
�O)�}c�4�4B@�3"��*�#����O��(n�<�O�3��������-���g�'��Z�)������6��S������(�=EN�l�T4U���_��H�qN�����S���&�k�M�E�)�/���dX�/�iA�wN�.�������9��B@�O@GB���p+:��,i�����=8k�^�Z��O�x~��k��{����tD'�u����g�=�g���09'���"F���e!>/���'��)�����w��p�#�q�b�
&W�)�S�\��;�K��i�zU�����IY�QTw�����/��]�E��|���������9�U���$�^UP�P��4��GB`"�T�q��PJ�"`���9�����1�i�:�o���������B�+�mm����S_x�e��������k������;1��rL?s��'	������5�RE���Ii���m0VR/������B������u����H	>~2o>L$L\1f�'�_.�����h���!�R�_v��z�R<<�%��������~�Pe�%c|��:�Rm�l���1�c9�#�N�EI�U��[�5���b*)�G��.[aD2	���
��~|���s������j����c��G^~
1��<�L��&�/��a ������5j��R����=�L����������6����XI������������{������`��No,��?�t�SB@�M@�}����@�%P����5����J���Xu�)��5����t�_�#�j�{��n��k!��h���b��:2.�Kk`q��J����W��b	8&m,���[AE$W�?������%2Eau}�%t�o�#���-:��������N��LpD���0����E�u��/�i��������}�kk��������n��fCtZj{���B@t����ah�=��|)�^��D�+WC�\�9m�=���g�~���;�Pw"�a,�E��[�HW<?�l���! ���8;�Lz!���TU����V~��hj�������V<��cJ�T�4��
O?*5
������������o��
�����b��?�A�4�����Z�~��{�"��R������#56tA&+>e�}V���:������7�8Z�di���B�3[����C�u���J�����>�>�G��/�pDO1�\�z3���y���H����1�=�����k5�����B\� ���#~�p\��SJs�dxl���B@t��������\���������d��w���I��������	���~�����B�	�~r���dh! �@�Z�r�{����
���_/���>�o���s��^�����������K�Abt&g�t�����4
J����)s�����[����n��l����p������>�_���?y���Z�R������#�=t��m�~}�}Xs����m���l0��:h�X��l�{�f_EOi�����������d�gv�P���:��z��L/��$��b��
4����6���o��]H4T������S&Q�'#�
����! �@���lOr	�<;����x�'?��b8�����r��:��W�B@�����c)#	!&���d�����$E�����3zFl�EU���%%�:B�>���3;wL�������5�Ao��<[Z�����.�E�3
16},F����+'��~�>p��^\�F����4�iY\3�:O�[����v<����'��d��T�����\��Q�y��P>>����H w�
H��a�(�N	��������������E�y[
8:��zQ4���r,����������c�zl%kD���
�\�[�\�����@��i�C�B@�"���@?�K�q�xf��P]��=w!����T��[�����h!��B�<q�)�
! �@�	�f�2�]'Po��V=�#�6y�O��H��U�=3��:�FX4�2�8�e���6Ec4�z��C�\I�����h�6+�5�p��q�0���S'*�\���H��*V{S�6)1����n=�Yy�������@BTR'V���*����`�����8>������k^�v�P��$���f�J���o�A=�HW�P����2��Cz��{��@���u�����]b���-_��U�)��'��/��a�~�op�����s�n*E! ���'+�X���}������������Xy�f#f�S������
���_@�~�Q����N�������B�M@@y*!qLz=(����5���--x��A����"�|s�,\NY~�cc{d�C�����_�g�>E�9F^t�'7�
2#s�Wv��-��S���n�����J.=�M�A��g�MY�����Io_�Z��L[J���^����~����P7|h��.N�K	9�����;=w�uwH)Y�#f��0o�����K��	;q�#v�U����r�ld��/���F[��������������Q�i=�knX�?M�j�2����|^��K�x�&�������#�! z�%3��DB����*��w:�����~�bO�����q7 ~�o���@���S2J"E! z���=�[&B #�S�����45&���2j�[P��[�����9����t1�3�����I�I��	��u�,
�]�P�iW�D�f�P8n�W����#?z�c��������/�=3����y�l����F�k/���l�D��He,��`+Y�q�>�4G9)��zx�E�*�q��@�&�4�y��|�Gt�v��:�f�j]��'��s�c���&����z����~����R��B�kt�# [��\b��|�b��#+�����v�
>57���� '��@@��	�!����o��G��F<��Z���n�����F���-;K�����g{�We4!#.7L��+�_G(�7����=`�[v���%�������������������X�qGv=���*qmx��cDj�#o��A�q�-6�g�6zgvp�=wak�/���>�U��"+��4�)�����8�\b(��B�
�V�CA�[��+�E������.�-������w��9�<����E��B@�~H@�Z�}��n����P}L�S�.�I����,�0�;�eN��@�K! z���=�Z&�C������u_xt��/����$������y��;�N�>�z����� /5�b�������b��)�S��m�)�4r��6�,;ZV����Gp��k���G����^
�jo����w��xJ����/��=�*���=o����\�8rY�/N��;RyEuE����qi�{��1c�l�W�����^���D���'XS�y���L0�]"_�N�������8����{�It*������ZW]|��f�����G�����1e0! ��@$�����]��KB[����T�Q8Lq�e����&��9��Q���������B@!��D�n�2��@�,�YUyc#���C��"���B�7��`�7^],'�%d'���)�jN����������b����6'kO*��,o,��C+=���[<}x������2��
�-��������D�	�����(_Nw`�h��Uk������/�^�X������X'R�������b	�t@U{�9P@�]�!YV������W=�n�����q_��F�!�������c��9PY����]�hGT�"���g���~����_����B�W	����er!�;�Sp�����YL�Y�[[�FNK,�&�����YT������C;���}$�MD4e�S�Ag �M����$
����}��"$���r�[�Q�Z��/�f���t�Q�����S,���b,Z������]oy]�/_���l(�t��r��4�v�U�^��+o���r?�{��M���W\�K�������@��B@_F�����gh����1i��eY��u������]WB@��$ `d�Y��V������{��$����AV|<����?���	>8���/��g������7��q����L}��8���B~�x�N��A�&��i��:R�����o��=�A�Y�����mO�	$���u�q"YD����������"��^wWO�>��l��U^���u�{I����.5�b�5�8(���}�3�W�0����f�����6\�����.�Fj7u�	g������P��$����vL���P�H;! ��� @�}��kT2�r�����}���[���2�}$�)�}���70�U
! 6����
���f��������|�����8�?�%���
v����a{��$
��������?+���~0�\B�`��S�b��J�T��@�/�hm$VCa2P�pw��~���y���+p%Y3�G����N�yZ������	��q��k��{���#��l5R�?W��53m���i��;�#�Q�2���O]}J0���>>84f���8��?E\v��N�B���}���e�>�����(n��R��k������l�g#W^�:c����]#! 8�@.`hO`���a|���K>��\-�����������y��
�	R~r�<��#�U��W��5�������	�����{��5iS�}���/l.��$a���������������E��r�#����6\���������N!���z���o\e�����	0h�`l����2c;5�<�n���^�eH�B�������#0��}d�g*����J�wM{�0E��>
����6����+}��B�����X�,!������cVI����9;����4Z}��������%��3�����zk	��F_66���\�m�`�\
�?�F�w�n"k���PD$y������4IV�g������u���e���� '�Oh�U|�����������x�D�>|-�T�=���#�������#�����q��w`��C���"m�+tG���B@�>G�n����O�cEtT����t�)yJ�>�;/��sF��fPx[M
�}�sUW8H_! ���<�o��Gz
!����bj��@�@z���xM5�5�A�5D��999x������[�=���8A�EV��Qx��wg�S��M������<�~(;�v����/+��O,�2�3i��B_(c����oC����tN���6�~S��5����m�A�#m�sh�uF#F�DKI	������!����C�N�����%�2VP������Q�?��v�8)��=u4����x9n�xJ����]Y��B@��& `d�Y����'G{�2h'/(/���
KJ��0��J$W������7�o�<�91�����=%�I�;A�t���;v�;�9��L9N��������K��~|-����=�}����OZ��IC�<O �b��I'l����J�����uV���*[�!����H������_-�B@�~E@gi$�o����X��D�}��t]L\�4�aK����V}6�����! ���$ `8i�XB�������t!��|9ZlV%I�o/�K����N�m��7���D���r��������EG.�>�7H��S��,ZK�U�Vz�?������]/���j����~��s���e!7a���B�����k����x]���=���g�;-���}�Y������O��X
�1����#J[K��\��M�^>Y�M"�o<����,��I������B@���#����'����;��u�(V�7v��wvJ|u�n<�icDS�m�d<r���^��7q�$�I��E���W�)F^�����=�4����S�4[���Sc}-�6�E�z<P�)���MF$���u�{�Hh��6z,�F��c�6'R�MZ�+�����)��gL����! �:}C)���a>��k�o�?�](S�K�s���L����d�Mi��! ���������B��	t�0'1��e�5�Z���������3]^��zL����0g��_�+�����n���bW\r��H@����
��5�m�}r�c�7�|����Ixc���
1 ����7�����N[���hr���M����o6�9'���������9q�6���z.v���g!�h]�9�_��Q������[�	! "����3�I��B��U�:�@Y����7f�}�#(KS��p�����B@!��	���o�\��8��
���5�;����P~w���OJ=��]��:��;����Rl��T���Y����Z6nBQ��$V�<���P�T�`�{�����E���PT�Sg��������#�jt��$����c�����.���F��B���f��sm0��?f��"�%�qC��w^��%�:����X}��C��#��D�e:
Q����I����,��a��GRnG���B@! z���=�\&�O��_��������pe%r0<%�S1%k�>��S}����~�f��~'�1���x��T��WO	At��q�������3���Cf!�\�����r����ms�X�Q���_�����.����`+�70�e�u���9;����[s�������c��&�~���b����-X����K��B �	���)��Gn����o����9D��\y-�.DK�5���#!�CcHc! ���B@�H��!����T#��������p��
�
��}�T\;~��v�??y���=y�k������D�������w�\�L�lE�Mz#����/-���������'?��n���@���~-\��u� �����G{p,^����}#���T��+�t�$���_����e+�=��+�q���x=��s~���QB�{m�FG��������C��Q$�������e+���<�?��'?���CG?���sX�����fO��}�u)�n��H! :C@��:CM��>N�=���	����e'z�re����=����O?��,����S���������;�c�l�������zl���(%&���)v�D�)���x�q���g_�q�kT���4d���q��;�S�vW�.*/@�1��^{`�[��x'�Wi�#r�>=��Nw�:4�8�f�
��:)R�!%:�IWZ�I;h�#����
JS���s�m��8y\>&�w���7%,����\���e|! �@���r���X{�C��������~�������C�Ic! ��L@��9�4!�]�sn�����>��nuxh�y��8���[�C��eKw]N���=m:��.��m������`G�t�r�>W�^�>�Zl���������M�_���`���p��H/3���?^g|�SH��u��/�\��-!�A.��2"��%:t�"����TI_���k�����?�	��L�!:�M�B@D4��K��{!�s,$����~���}i�8! ���" `�H�8B�E�y��=UdQ0Pa�PK���{'9DzL,VP���~����g�w���
��{�������v�U�SQF}H��R@�w�������+��=%�C�x���Zu����xw�zr�t�K�v�1-�#�HOyv��������u����� `'3R=�Ov`W�Y:D[\�~�������F��w�V����*E!���������`�(�t'�f��Fq���7��M&�^W����R! �@?% `?��rYB�=�������~�y�.��=��?SW���j�<z=��g`Gi�O��>xn�5B�}3��`�5�r��������'D�fc��������SJ;;	���������>}������apz���X�����+r�5�h��D��z��=�hw��
{CJ�("+@������a��H-�/Z�K�u	! B"`,��������M{{� �_�X�������T! ��hDhw\�W�P��s�.1�4���~��(���9�����
�?u�\�3���c�>7Ff� �E�Z8���4[}]|�|Ms�b���Z����>�����l[3Y�i��Z�V,��:���o�����O����j��X ����L+��x�m����}��	_�5x��D����6u2F^��G�!�! ��@t��H��G�Q�Ymq�M����hw��Z���B@x"�������������h�=��x�����E#���1;����NIL
��}j�q��(����3�y��(N���Wy���zK������X~����s4����N���c����h�W�T���|�m��
|���l�B^�Ui�����>u����^���������aml�1.���+�F�D�y�5-eW!����VqE���Y�#&
�W���m�I�B@��N@������B���Y{������V:��h5���=���Mzl,��>c@���V�����3�c}|�#���-��f ��'�W����.���ZLNT�Y�-�Pb��>�GY��g�rR��g�
n1(��V����I8��A���;�@��c��������8>{�{j�^��(�f���H3#o���a��K{uM2�B �t��>q�C�:�I�ami����)8�9'B@! ��5B@<���+hmK3���;�����6�>�_�Y�e�V=��2E��1������E��,��f��������v����_�
��xN[q�
�w�{FJ��C,y)2���c�t�]����#�\�Ce�
��C[�Ot�J��Qr�*r��vq�)�c���X���BL����5�k�����c�$�����0���{lJ�H!���u��L�>���L��w	j=F��)B@! �@@"�"�B@0��g�����
c7%��X����������mw�0���h��������\~�x?�z��������ER���
H��y����m��\��}n�	.��HB)Mb��1vT�8���A%#�����	:\�	Y�Q5�������|oy�!��>[����91C��_���Y��$`*�����S���6�4�s?�|��~G��1mV'B@! "����s/d%B���b�b��+��bOS�������h�iY���\��_�����;�d�$F��G�){c��C�|t���-�t����w�����d�z�����*>�kG�|��gBl#����{z���-��|���A���Ao���0������������m��:
f�]�;��/������B@D<����H(�O ����V�-�����>����Qxj��p�g�+�/������3"������I��"��rb�1'Hx���SY��&vl��5���.��L���=�s1e�Mr����_>��:�����#z��7��d>#��$a�p���c������[&=� Lqq�<G���P�B@�-�������t���^s��6�������I����9'B@! �@`"�"�!��m( �O���q�?��0�4�a�$������p���9C�1'w(��������<���h���l���-o"����[���w����3�4#'!'`��TYI����b�1=Z�]n�Y�������!����7�������Q�!�Ok��c��}y��3H �$�/fP&R����?������Ge+�	! �*C�a�n�������)��A����*n���
jOCo�?�~��1A�	���F��B@�K@�v���`l���������
n�<�9/Xs��0%�Y��2���x��nX����"pe]_������p���m���H��?>�
Ed��B_�JJ��k��o%��huB�aE:����?��TM��������S��?V�{z�3�K���/np�w{���'�@�����a>�Q'>���yh��~���#��0F����(>CL(���B@!�! ����N��}{������[�����c1*5-�A�e��8�6=}z��#���Z���I�2�p�!��6[���p�W����8NI=Lz�"�q��>��s1��!�\�=��>[��&r��ja~����&��I����Z�c������K_}��a"S��-.2�}�������E���/�Q����������w��R! ���(�#����b"����?~���@��{���6�|z�(��:u������DXI������[U��sn�,��FA���wA�E��|���k�hmB3=��'w���d��myc�Z�/���#�h �@��.������oJ�T��[��pr��9Z��@]�^9��l�&����v�^D,�����=�G��;oC��K�Z]��O>���!���o���C�@�B Lv�Jv��������@�v
cK�E�m��:�&��B`�p���0]�%#G�w��[dEQ���d�"q����|�4��r�"x�m�]cNu��=p~�����UK��\L��)-
�i����,�J�l����*�����$�J���
�z>RcS���~�3�������8��b����T����,���jv��+l�k0q��e+�0��n
�}�\o���z����f}T.�`��W�����L�G�80s�����$��j�>��w0q��a��o�e�Q��,�{E%.;����!���xB@�n#�I<�'�"����������r���I�=>��s�����������5m����r�a��C[
! ���	h�rn �B ���l���Xq��d��h�h���+_�-O��xn����?�n'q)����C4���^v��w>�����Xi��w�Dbt�T��$�q���ZT6V��[�AVb6����V~UM��%������}�h�3��1g�F���!�.����Y������H��Js��3�,���������
�O���
m0�}�!����qU_z&=p�RRzhF�F!������\{��O���
��!h���`20��r�[b�������B@!���m�+W'}��@<�b[6qR��!���@=%l	�h]�C��]����clzzw
�qx�^���5�
?Y�C�e�A���[_Ee�U�5/^EIN&!��(���Sc��V��}k{�F�W�<����t�����+���V�Lj�a�9NL��Ty�(���D�=(���u�?�G\�.����7i��a���)13yX\|{�f��=G�����7��B�����6������C��N������D?CCI��:�G0k�9$�-D�����
��4B@! �O@��3��@�(��j!{7�d;����-���{���r�>���=��:����^�����d��8�\�>��6����=�����_l3��+����tNe�����@z���:X�N�u�J�V���#������������F����\����;�W?��jC��)"��$���@��W+���? ����Y� ��~x��Iz�
��e8cR�<�N���w��kOA�8{oe��8b��:l,#��o�pF��uLi'��B@������(���&���X��W��M ��,nzV6f
���D�n�9�'#�H�����;����U�V�����u�n��a��h�����0��<+Y���s�,����0����:��K���?�?'r���Jr"�����Y��Z\�@IDAT���VR�M� ���v�Kn.+�!��2�d(E$z�3V����Q������M��=
��&	t��C���<��qE���e�Up$�����m%��O�^c������m���Vr��
��%�1����B@!�a"v�t}���7��];�Dn�������vK���x�-����������v2;1�M��e���)��7y�x�_��P>�������s���}�)�G�E�[���&1�N,������G8Q�b�u��@��j��V�faEG}c��hUNL�����).�����/���,�g8�$z�Y����SJ��5����w�Q�Y����+	�JG@�����Y��l���g�]]WWw�e��#"]�&����Z �{����N�O�IHf&�9�����o��;s�}�����]�
�N��L�8\M6�#l���>�/	��P�uL�mlU%�?��Z�/���Q�)o��	U���X~$��B��m���`L�	0?`���yh&�T�;|������Ib���p4!����?����������-���|��{����8�m������R���s��7�����|�����v�/���:��UDPl?G�
��e~������kQz6A�q�R�$6&��	���`V�@���%}�����a�o�P�����=��u��������/��fm$�(�*8Q��wS2Du�x�Q,?��*
����`L�	4+,6����eu�*)�+,KM����]bb��a���	��?O��2J/���OB|h"N�t:���]\�I��b	G:�a�=YGm��S�%'T�P8����%���<��cW/PQ������g"�Ps�
� V�J��%�}�^{���(	%�����,�ED�����;`�������)��ww�'�f#|�3�d�\��2�_��K�.:�ufY}���q<��������\�	0&��L,�u�Y1�z�,.��h�N���������5x=+��on���w����E����_�Wd���')�W�����EvU��!.�G;����%a�A!�Y���f��|����H�#+	��	�����;,CG�C(AH0�����d$�C����%��U��V�_t4!!�z�tt�6�y��2uU��_��q�@@�\�GH��[���,�}2T���Y�*�������RU�����/�Lzo���Cl���m���`(����H��`cL�	0&��?��5�3h%��z�������
m��v�7|���I��H�5�R�@��G^�,��0�[7�:~�N��sR���C�):&P����2
��E����Qo�>����5q��R	afDU{�iI���N}��4���E����*��P�b�E-EL�#�M�Uh�}�4#P���2,����g�Da���R��?�kWe0Y�FHb�����@s$�"���5f��(��:� ��K��:yRs<������"�Z��g�HEE��$�M�!�?�-?���)�:��"������r�����1���wXCR?Tv���"�G|O�~.�\�=`L�	0&XX����a	�xx�H�3������.11�G�eO����Q�y�Dm�(�?-14�M�=�����IN��}g������Z�&��(��s(7�$��F,��@�\�o��=Y���e��Q�g�X��"��m-^����1@��� W����JI�a����?�/(>��-BRJ
�|�~�,!�<T�x�-�T��	0��PfY��o�$�������b4��Y���G>��w{����T��(k�����	�=�w:��7��!�RO&��Uv���	�<U���u��y�������S��|fI�x��{����a!�n6&��`L�U`�U\f>��N��-�IX��N:UP�Zs�D��&t��^;�����U5�k��J�d����W�l��dt���e�u�����Ko�D=��V#�P��<E�^ac��������b1�@���O�x��x��?C0��#��e��3�g
	���Kg~����YJL?1|���q���~�	��1�?q&r
K�����)�]��7���M[	UQ:*�OU<��Z�?!R�+��"~>�:����P������J�v����KO���W�cl����x���a����{ ���I�8��GV\��`L�%�?�����sc��@vi)f.\��bJ�ZO�@Kl�%%+�N1�������Ixt������2=%JH%w�h&�	9�9�8I�5 e�"h9��35���b����\�B���b]�Cr����,�3�k���WI%���Pll��W��HO�������(������1k���L�9p��'��&@
b�t�%	t���.���^�	#�2�������������O(���"����
��T%�>������Wt�k���v�!"3��^lL�	0&��@�$�`���|������4<Bb��E���@g������l�z��[���U>�M���-�N��"~j�D����[n�w2)���~o�����A� �D��c���zfh=��$�Rc������lG��d�U��h�%���}�����COq��T��+mJ<&�T����M�M(��u�u��yJ�dTR%%����~f�'6��(��A[���1��2��3����@G���E�zyw��Xd�&������s���;c��w��E�>�
���+�B�p!`L�	0&�\���w�@ �������Kn?�:��l�j�e��60��uXE^�c;u���m�r�&����N����sQH��
)P�M}gP�/����m��V<]��&���w|G|�V�2��!�J�{5!���[S���>:8����(�Y����D�;4�R�A{%�Xg�s�[�D�������$�s�<6h������q�~:��lS��)'�a	��D@��?!�y�Q��qo���v���4�/u+�g�0#V>
���n�P����2Tv�l;&�#�����y�V��FE�I������3�{���0&��`�`�;N\�	������7x����#����dj�!�CM��Y�"��md$��4����:p���gX�oy�U�?���%�p�WSQZUB	X�q������/��Xx��@K��!D?��"��F�N����!XC�y�]������uW`��QQ�~��\�{����=����������7���e��#(.���x���@��7��S�lW}a�m�)�������Aut��V�G��3�{��N��;vq ����}M1�rP��n��=/}�(&T�:�
����OM�dM��Y��.�)"Z��A�&W��m0K2e����&����?�2z�1&��`L��	���D�?&p�*
��ip/�����������cR"p�V��nK����Or�E���w��W<�,�u�DK�o���S�*)	5C�~�^�D~�F�F�p���(:q
�2K��]+7A�ia��o�iOW@Gq0GQ_!�������wh�Z��L�	�J���\�3�m-j�s���?�2yJ�R[;��:��u�A�1_��������v���;���������=��o�Q�kJF�M�*��&}#"�e��=��"V>B� d����P^��GP�21�nr:.��Tt�����aL�	0&�����Pc��}1&Po�}���������pYKn+H`l,���+[��'xl:��I���aae!4��5�_k��s�fl`��]���P��t��'����>K%��w���"�}[$�	�_#6&�	�C���Z������\ 0�������<j��H�;����}���%W�d�%����_i���k��}o%��0��(u�w��
�t��3S���/#��|h3��������j��8���!�s�VhN�BsX�dG��v-*�������`L�	0&�hXl4���x��}��3������O��.]�jg0�/;��au�[��E��X\��������s|h�k���6")c�JR������r(!G��getM����p��	��HkgB~�S���5H�����b ��v�a��t}����Z�h�����\�e�T���v6��3{�$S��������=���=�Sg7IOM,d��6��Z,W t�{��_T�#�@�{[��m���D%A�!�/�������Xh;��Q�f0�&�����D~+{���\��>`L�	0&����MA��lU��l��#H���H��7�M
rhI����,=|Gi9��}��oRR�\����>Tk��N��U$zc'�����0��N��v�,�~�K{o�^��be!=S\�L�(i�l2#7�27��h� 
Mm����"�Oo�P"���]�d��	0"`�p��*����D4P2���]M.>��0���������;�I��Z����.�������|�3��E�>�?
'�����L��(&O��r�/$�����t�������20&��`L��	�������I����o��r��<��X�v������$Q!�<��?v�M��:v���}{����(�\���^��5f��8XC(v���f4�u�7�@K��B�9�DBn�z{b�F�����"�9&��*�#)!��"6�`��I����u�}(:y
G�!�o/$��=sL�� �>3�L��M�uB�<,�������P2��� y�G����>��>
%#q5�z--���X�z������x�?���Vv1S;�������?BU��^����`L�	0&��Xlj���&�SZ�7�nFY������J
a��~�Q���(�x���D��~��&�m�OXt�-�l���yk���RM�}o���b�E�gc;���9�b-]4�C��W N<���k�Yn����G7�Q��1�]����U��p2��U#hi��]������=����7�SG�R_6&�j&�,�%4Sx��H����j���+���J�Qf�Ax�[]*�C���?�����vP�#��'@1���[_0x�W�-��(�_����n���x]����
��?����KF>���x�[L�q_]E���K�&�=�5��L�	0&����	��{�<b3! ��F�)�x'�������Y��y���W�i��V��IX����X@���Xjx8��#PJB��&�������(��KW]�8;���Z���x��y��9���F����X~xi���u����|V�C�G;��
�W7���v?)�T�G)�_mv�n<��w@�������6*
�g���K�#8>����"��mB<0hF��
���$��,��$t�Z��g�.L��m{�=���h+s��$�:�k�E�����e�&5�Kz��D��&����q���+��kl@�:�7-&���	��<B7��G�(�A����Z�?Q��	���&���-�Vv'��L�	0&��hb,61`�>������b�IHDb�����6�O��X����ID�tOJx���BO*��A�7-����O��UVW�_O��0�*<�v��CJ���}�3>���-���MA��h�����S]���{v��f'0�:V���C�����E�Z�B��#�9��cu���`�V"3����v��"a����z�{W`N�g����x�dl�y�^BYno����9��<�q"~�d�,��*zX,��OAE���Og��,l�<o(���^�_�h���<$r�JD�<>�98�6������AKK�
���o?�v��
�.��mm��`L�	0&�|I�@_����J`������_��bJ��d�Lt�L�5�'-Ox�$���^���}��0��`M&��*�2���1�|�LM��,��|/��l�i={"H����W�2'3�BB���Z-n��3(�oK�CYu�"��sU���c���w4!���`������������/Z����y����"'e���3N�u���!C���PW���(��w�c|�����Z�recL�a�7��'����L���gOwj�C9�K���)��;W�m2�Q��{�+@%#��C����L^{y3��9,��9e��te�-�{����<�p�{�@���1�3�����`L�	0��B���r%�<�Xv���1^���/�-��|J�a�vn��'Zw���T{�9HG��O�^�X���n�v�qpJ
�P�����S������}������`2������Q&D�ZVm:��c��4p���7!/�Ls�p.���$3�%L�E�x1��f��b���z����������P���B
�4��|�7�����-[�=�yT�IV��������5n�<!&����!��Y��BZ�q7�*�a��x��c�����^Nb����y���Ek'�Q��f5
��g(f�T�3g"�',�T#���o�����S�,,�G��t
'�C�c�P���`L�	0&�Z[�U�s�2����8G������F�KJ��L��_G�BI-k#i����S��2����k��g&Q�@v6�����2�K�jnm?r+y�
Hnc/�-�	���-��L����Ngd��QE ���-2���7J���%�;,C,�]:���j'���Si�dj�����j�f�p)����dl`����cR��V���e��z�3���/ ����B��e��j�D�����;������j���L��4A�M$�'�� %�p5�<�=����t��{��A�=�h�luB(I@��<f]��F��L���yG�=��2W��������L/Clw7�Q�)=��S�vy��K��������`L�	0��K����r��_��������YI���Ok���7!L�9~�/&{�����u%B��d	}�/�uc7%�}�"����<3jtC���6���sw�2})��<3���y��*Ji��;w�iY�e��1@��	����FX���u�2
��z��^�f���+���!�@absl��q7��y�s���'L��?�C�~�|L����r�G�ks�(t������ �C{Z��y	�����0�K��g�d�_���^���>��.�Q��_^@�����5y�h��A[���rY������L�9���z\��4��1uMA��TXU�9�9�I�+��#�����	0&��hU���U1o'+��Z�?q�"�n��H��N�����.*RY��=.(�hMVHK�E�����V���]���=�w���B��xj��J<9�O���&�`���b����RCMzm��VsM�j�;���/6D��BZ"H��)��?1�`Z�v���a���v/�2r����pd�Y�����xt.0f�BH�$�?Z~ouRm�lx���`�F�a�~�#��L�u�(�Q,=7��B����2{�(��n~Eiym=����{���?���!����]��u��*���(����z����#.gL�	0&��@"�`���t*��v�RJ�a0���e���R�	r����ML�x�fUFf-Y����Uk����X�k��%k��z?�}���~w-KL�e����,P�����~�J����7���:��K��#����=��������|W'?��J�����%���Q�$%�(�>��a-�M5yft����}�����i�F��4�<��	��/?���<&h�zD-�����A�sy�,�D�q]M� J@�vb���([�Gq��������l��c�,�P��v�*
�;�=<y4#;(���V�q4�����`L�	0��N�����!����L�K�$�5����"0�]{|8y*�>�.11����������������P\Y������� ��a��S�K�O��;EG{Q�i�|��g6�O��Q�����h��i���Ov|����"�%�����jk���F�Og���t��#��S+{��7��Q������)�}�����hL��������h?i:M�
U��B����|M ������g6j��(�;��uC"������!1��$2z2� ��8O�.��L?��
�=���U�K�J��_?�L	��	�����)��?&��`L�	0;��'O��{����{��`0�S�N����1x�`���=��s�b��-����'N��Y�l�y�y��-�+P��e��=�,�reaO�Y�����������<'t�&��b��`�q���`��<;�r��w~
�����49?������[U�(�p����_����D�~f���&z���3����Q� ���x$/<����������t�4,�����G���� �����&�#3��M@.�v:A���z�H8��);^
��C�P�C��Dw-Ui����)����s�-h
������������*��e�oA������K�`L�	0&��	�T,**�c�=���������H,\�P������b�UP|���{={���o����4�����'��w���/�~c�$@��'�^gH����_s�Z�������R�G�].����<8 y�?��6�N�CBX�m�)7������@�+w�A���u/��\���}���*d��a��[?:91f|0�
��eTh�H��0���V�k0��bx�+������	k�_:@I���Cp�L�	�B����k3���nJ.����C�KU��	�!���:�����Q(fc�6iB�7k=��bA�����1��>�`L�	0&�b	�T��arrr��K/��K.Q������O�?���_�5


��SOAG���u�����`��r�-�r���al���<������D��:bx�v�0��M���f���p�;��"�i��7���7� �;���M�F���$_�j�������Om@.e�,�xO�T�����i�vN��h�����"J)�n��%�GW--�����
�\D��}=L��J����'��R����b�yJpb��w���cM���bk:��L�	���1�������$<�<f�5yJ�]l��uE��Wa�����	y�a"����o#���#��d����^����`L�	0&��	�Tb���?n��PJ!<KKK�k�m�62D��g������Oq�b�����Z��L�kw,^��}���K������Am�`���^����7en^0��n���'b��H�H%�����c����NCO^~���Ub����N�6"�������	<���q6���6/�+��y��f�4����v����A���$������$6o
���Le���	�9����/	��!d���^���/j�����WUp�
�'@��6����@��K�w#�#��4i���#d/Ecp�n[3�tS�N�`L�	0&��@�|*v�����h"`ff���'�322��O�*���S�ss����	�����lei�U��6?H�?�d6n�"k���'���D^}��X�{��A�Tm�:����1�w�@$�(���g���:�����,�-�������1�O����&�1u�Zujk�z564�`W�oP��	j��L�~ii����^|]O�0y�l�|z�L� �?���m���L����6�[�$��� ��J�:{U]����gC*�D��i�����[�{���]~��W{���)q�&}3e�0B�n����:��7�\�S�7Uk�#����S�A�*��(+��"����0&��`L�	4��O@�Y���)����k���'+��'�H��ha�"I~~��x���x������S�B����Z����<����T����r�XHi�����!z�a����IIXv��8��Wcq ���?t��$9�A�����x�z����A�AN��,�"�g}���������=��/x�������@���i��M�!�<����)	�_i���3WmWRR��L���O�H@1{�Pa�=zv����#��$���sa������
���'	!$�M�������S��X
�|�|�< �7K�m6�j�I �\���w�FJ��J�_����LI0b��@��z���@��5�]_�m`��������/���k�:t�u_��mz����i�!����`K���N{�n�j�v�t~L��!����[([h�w+�����"S\i�evH���4���J��q�����T�`~�����O*���y���$��!���/�[�
-���������d�b�q~�8���S�"�]\��k�B�f��������#�����3�p�t(��T�9�C�w�@%��u���|��'����M/f�W8�����:�{'�N(q��E��M}���m�bCx�;��?���Lg'�H�^e��h��	O�x�|�����>�XXC"��>�����O|�8=\�]L����w���8#_����UM�)
�j�Lz�k`��81g[�9�����7+��&8-�5V�"�$����
N�����(�I��.����:�I���!���"`���TU��K��?��������~��{�1��S���r��q:j�q�y`���j?�D[f�L��0��G�vC`��
�*���^=o������j�9r��	4����'.mj"�������RS���7��x���J��gcL����XH�D6`����7�@�.��
o���b'�%%%�~�C�3Q����R�Eva~HsBv�;������=�:��e����c8�g_F�V��]SMkXj*"�kO�����%:i����bI����+��"���|q�*������\���/�.��Ga3���W'���f����W.���+���O�� ��]��5�_(����E��J�v�����U�!�Z��W�y����<x��[+���~�Q2���)W��]g�g�h�����i6�XB^-�<�����m��,9���`V�A|�k��DI�"V=_���r������|
��V�X U� �����,��1DwA�����d�B~���NAs� t�$��CB_���D��}jh�?�L	��*-����*�Y_B�$)TKY|g�S���R��R�T�,-��tyl/	4���e�\��	��YUUU>�A�������zi������%��B�{��G D�w�};vt:���X��3��))�<:5�&��R��(�y����<9�
[�*��������j�[x��=�Z\FB�_3��fi���R�������{l�7�Z������Y�����gmu}�a������*�\��"���Z������n��g�����#�CRW=����0=�h����2K�f�������%�D�c6&�R	�;��}/)m]��h���j��L�/������2�Asn�m��e��?A��D%M~�������B��
�
�tjk�s���#C�^��or��D;A��|m��	fJ|�{�j�"��� ����	cd;E��
&��`L�	0���(��'�xB��{����Dq�\m%:X�d	%�4*���[�l������e��re���C`��L�Z��q:s�e\���,)U<��fy�P�C���2-�@Z3�nd�8n��H{��.C�u�>I@��w���sC�LE�"����$�?'#���(�2��9]��N�d�$���]lO�k��+�a���/=�������q�=w�@��#:�W����xL��x����Xb)�\�]W2�I��lb�\��T�����0�_�Cv[�ZHt��#���PH�CT{hID�d�$U�#z�D�\�t��z���L����DU�`�N��������r-�B�+�?�nr�Dsp4�bcL�	0&��>�/_�C�)	;�o��D !!C���)S���_�����}�����4��3��{�":5�&P�LZN>c��:j5�py��1A,�^X�3�{�'@~�,�%���:�(�+����SH�L�`��Z��L������u���=9�K�X��VD����+0��9���O>�V	�w���O o���l�59�hBR@C�%P��h/���5��
l/�8eCTG�����7��&|�����6E������s��������d����n��.<����Q����C���#!�JR��	�?���?�q���D[q\�E�^p�7�\�_�
�`L�	0&��@���D4��V�X����w���.�mB�����^zIy	O@����+��u�]�D��n[
�_������ 14O�b�=�zU��^�D��G��:N��hL��w����g���=y��a��n��x�l�����W�#�����;(��=*���^Q���`p^�k���x3e�5C+����H<�����yz�#nXe��zZ�k��B��|��(N;�����o��@�~�<���xw�k���b���y�|p��!��l�(��t��Pn`��3��E�������X��*����a�m/����;�8 ����Kl�)��#��X6�A��1�����i�M}�6}����*
�&��G3����c.[��T���T@��9��� �]&��`L�	,�
�b��76l�0,[�LIk�$o�r��C�`v��w,7���wJ����A|>m:���h2 /��?@����Q�qyj�&�KSu����m��c��uNCL���Nh����<�$Z�{�Ve����DS\������t���j"����t9-AEY|O�1���xgV?D�t�z�������W������g���x��"�����o ��LL��C�*/��x� �G[�������O��{c*����u��Q4�����������m�v�Pg�w��p��
e���d��n6��8[Cx
�o���������!�����.f�s�I����E��q$�`L�	0&��@�"�S�>h$��JNN�O����!���%�PE1�<YQU%�����C�R�����H^}���>��Q�����j1==h�d�@S�L�4Fc���}J�����t��-�2��81vL������b	��&�f��z�>>Iq�p�������*�
x�I��C�}�����g��d���T��y�����`
M�e��?<I�2�2��7��G����{�V��G�~�M�����$�T�����^s�����\��U%B�����0$�F�}��We.�,"����cPq�
�6�7�Q�`L�	0&���@�
�Mr��i�!���m5�M}����=>�i����w���Ne����{������5���9���6������AZ�KN7��4b��&\�3���,��D@'�S!�T��!/�k6�Va�[dG��e���#Ax���8������x�5RY�[��O�#���&�C����tbz����x,&�d�7���n����Y$�Z���T.U�	�nK���t`�S���YUEg��=��c�98Ur�!�Am��Z�k����w��5/�ujt�;���t�^�"�w��w�\�U���`L�	0&�X�k�3�@ &������v����>�vz��v�R)����b4�55��W������-�}������R_�E&pH�3m��H2������{5y�IJ�5$���Lk;��~�����&��To��������g�C>���?�z�X�c��oG�_����	4W����x�i���>�?����7��t:��NN�n;^���������}J��K�tG�S����4�1��!e�x����E����S75�8�q�2��o��rE�rS��K��!����L�	0&��`-��-���*(@���n�������R���p)�9\{�&:z�%������z��x�m�����-}������H��KK�b�zdnH������|���������iy�05�����i5�G3��;��P�3SRK[�����������&��������n7�k�
�#1�@ @�	JG��m&���	�����%���r���A���h $})���7�{���6�&����OE�G��H��@sf$�X�%U��(��_<���B���^��k���e�B]r^9(����D�����y�	0&��`L��`�������0��~��!,�<�V�y'�*�>�m"���������>?��$Vz�AmR�����#�,z���}�����6�\&�����:E�J��9G <;�t�S?>��y���
���MY�Bb������69=pk

)�o�2����7����B�sh0j'!o�g��z�e�g��>{��*%�ox��>�b�D@�?��E3��-�����h�G�����/C�~�[�R%--�6!�	A�&��r;t����CSH
f�"�x�?�G�|=����Q�n��~�Z���`L�	0&���B�;��a1�F&p*?s���-��*+�������9���L��h!���l|��\{����`����"G�]s���xn�e`_�n\(��2���J�
���a����a��������S0~[���g��-����2NR��*PA�c����
% G!���AF��3a�jZ
Li}C*����$�f��K�~�w[:�=���2�1=/AHbBS��n<h"�+���%�^���ah;���b���(�����k�B	��3�P�����������nM<�m���P �Bf�'Ej�1<������c^T�?1�Yxk���F�.�`L�	0&�Z*[��
��*3�m��u�����+�S�������4�5�e�#>�i:o�^�g�VZ�&���n�;�&}6E�,�il9�����n����_��p ._R��Z�^|�y2���G%��JB���Z��*;M����?Q�l�]���$���MI��s($��=+�����A�~��������T�����
��>��|K�P�E/�9wm�HFQ����Ox��E����_��;~��b��R�u+s,"(���L���F���axT�����`L�	0&�8Xl���{2�c����0#"n�FE���[��G�������de��%�=�Ea�(/F}1�z��d�B�?������obh��}�T��s2���Ms�/F�p����D]�%!�"_t�����(>y���z���b��J�������|�����{bMH r������w4���fH�
,v�f�{l��R�P���-mmmM�0�(3��Y�D��\s�7Tu���
�8�n��yF���`/h�-q'����Q���d�z�����hT��	0&��`L��pBnm�|�����<%!��dYz���nK-��i�q�h���u�V�6eg��]�a��#��>����97���1c0������������X��'�z���r$T���U{�	���vb�oa�����S��#e0�1�Y���h2�������"i�P�������{s�<�VH@d���Y�=x��u
���.Ts�-���:/
e����0H��T6��e��Y����L����d��7()�h�N����*)����I���tgoI���'��f��k
N�s����
�Hb��#�8�<dcL�	0&��`�L��F��Yl?�A1������t'P�%����U���<��'�b�n���94t�����L_���;>t+s-��D~y�kq��/
#1O8�Vk�b�oi�	)$<������y�q*�\��!��#����1a�\�"�f���X�j���Y2V��k�0��^�{�P�k�lB��W��������K[���7 ����������&�b?�\E����oZ2������En������W��_,K�E�K�G��'�z������;L�	0&��`L�1	���4�/�*���X%�g���TA�����0Rh�0Q������-���1��=����O��������a�#�sww#�S�����(�WT�D�~BR�O#�/�,��A��[�5)�
�'CTgh�v�ZUt���]�F-?�ol��'!��J���\�m����t@Ui���^����hH���+��%)�!��D�@<K^10��]��)�aL�	0&��`
 �`�q����
7��F�_�H�[x�r��Fvs�EZ^�����1���7|h���CY�A '��tR��tZ
��#y �GKM�y��5bWof-R��?Om}]&�2�~��zZ���A�	��
���lL���;��$���L�;zV�r��	��0��P���_:�<�M�p�
��"�)�)a���������`L�	0&������{$���UZ��$�����Ap��=��u�����-~e�9:�N�� u���lM����X���bt;�|@IDATv?.a�.5�H��X�+��PA���G�m�q��BXj�[90��O������������,�^��I��g�mK��e����M���~���,���`L�	0&��@�T6���K����`B�s��wl��������]7���'�.Rd��8,�1�I��e������Nx6�*&�B~��}���r����&��|$�C0��?�����c��@��9��v�l�7zW�s������c��&[������?�bV����)��!��&����.���`L�	0&P���F�uyo�N��2K2

e�48��X����X�V"��KzbJ��m�B$��A�%����w@-N���3+��%�"k}���`���tz	%��0�!����g�����I�5sg�PZmp0��Q^N�
��@K$���'��DP������W��1/Q�h��5�~F��{I���{��D��&�Q+�*�h���0&��`L�	07,�!���  ��on�	{33������wK��xr��hn�f���%%�
!V6G�"���e�����2��07��
f�O�%�G`x���9����u�M�����B�L��7��;�����\K�o���|
@���6����Eh���YA	-f�:�����b��t�V���oD���P��@���Z�.�="�\�I�;�a
��:/
����0�l�lL�	0&��`L�9��S@s8�c�Xs���T������qe��p]�T�G;7����y�m4��������Qe������\D������7q��'m]���/$%q}�"�s'E�������"@y1_���������"��m$�y5���fA����:��)�]�OO�l�_`
M�������L����7@w�;��kr=��}!]
�R�0�\K�=�rJ����`L�	0&�Z[�U�s�>w���g�=��
���=8h0�8d���Q��;�n��������_�"�#��X����w��(�������C�|��=���"Z~]Yc_�~ ��{DF�^���G`t�pt2��\�L��9_B ��i0@sv�"�Y��JU�$�}K1�f����<��'�?aB�&�Nwf#�)��h��ThOv�>����.���xz]]�������!�*�_�x:����O�&���`L�	0&������;[|��G�/P�t��	x�O������l���j
f
�?^����I�}�\hUZd�da��mu7��u������X�x���7�q���-~:����J���r��'��]��>�	i��-6J��(�
����\�"�H
F��:����7>I_�x�9�,WP�/L{|��9{�	1PU�
���(�_eW��G.�@�����V�^�6'�6b%!�����{��3w���X��:4��`L�	0&��,,�����v�J��0�M
BH�(�������*o�^�{Nu��@<�XJ�����O�����\��_?�����}]�)�}9�V�����/n��X�l�d�[Y@����{dt9-C$���Z=s$\�ME�����`�����}Se�WSn�\���(��.fhI�[�&
���[�X������]��3�������CQ[>��T��t�;�;���wTN'���'�u����`�2OrE~�B�d a������b��c�*w���U���z#��7����_U�`���+��=��Q�������fL�	0&��`A�����?�7�l�"����L(!��ctN����yt&��e3o�������-������y+b�C�&���n|��C����GF<�a�����e�����9���p� ��;>�������;[��r=�b�o���B�c2Tf�4p��j��h~�R��
���J��gk�e�����]zQ�������A�Y�4�l�k���[k����.D����"+�te��(�b�[;��)����e9�*�`�9��L����(H��vwmj��L��-������t
�=m��a�����J������U�F�������2&��`L�	0�`�����`(���}�(~�=v�q��x^>��7L��QAA�����dz�$���ek,��6���~�L�/������z#T�.R��A�/���k���	��.Wy<&
U�h2�x����&�w�]���8��*I?���[+)�8��F[����	2��a�$����]L�
�>'��yE�[���H�2��|.>�������Zc�KW�0�,#�=-�5�p�����J�>���_
I���W<�������������:�:�x���Nx��<}5G�	0&��`L�	������L�b��@OS_|� ve�8�O��cT��)�D�	D�Ly�����g]]`V�|�{6-��~���6<���+���F��T���OR_l8��[�k��s��)���"���jI#�#���Y2����mq�8�w+��V�xS��W}F����D�z�u���{B��M**��v���S]hiyo��?��'BV{��u������IC�!L2��������&%����Ah�*�g���+[�D�����@e�+���J���rq,l������������9�}&�0�����P��Ky�q=xL&��`L�	0��A��j��u��,���1�S'|�o/F#�G�-����O)�L��]D$��|��i	QNr�s��c:y�k-,�*�����{������.QD>k�n���;��T@�=��4<5��*�#�"=�{[�+�w�U�������]M�ykmZ���\��]�$��*�`b���bM^�ef����8�P��"����O	�B��q���|�����q?���.c�m����_�)��#l�KP�xW��
�yT�Cg;	/6�.�6}d��',d���8���n����&wk��_<����n�]n�e���se��|�c0�>�����@qqM���L�	0&��`L��`�����#1�}�H;��z|�BZ��;����{7j�k�����t�<��>�^�*^���M���D��.������o�Y�Y8pa?.Mq�>��g�r�9j�Z����@'��M5I���j��c������\	C6�A�1��$�]��X�X���������g�?�z
R���3[����!�����SYCw�J���L��W�A�'H�ak6��P���u$����r���P��1,	}f�o�"�o���4�%�N��y,���<$�:2��O6vf�����Q�K
B~jZ�\��VT��b��[L�	0&��`L�	xM��'c����-���v��?9���QI��e��UA�����!���q]�b��3~GqM�.u��I
�n�m�Zw�|/��oW;�}n���������E���iy]��sl�����Ur]H|�p��N����	�i������45g�����
���>K,�[�i��4=�����uz\ !�%������L�����/�,�#�d�CP|*�s>�-�q��!~��^��*���]`����$�+�l��f�����z�U����3�Eg`ViI������)t�s��o��8�r��������WJ�ND�t���<9���h9�a�t���m�?`L�	0&��`
&�`�����!�a���2���j�
B�o�m�n+��F��$�����l����n}���m�x�&}v
^G���3�:V���Us���������"�9��m�y1��m>*);s]v~C�C��"=�'�H$Z����
��NWBxW^(�D����_�P-����:�J�y�'p��h��^�Ii���-��&W	|.�[S�]�,�\�
cD*%����#�)�0]�^nF�<v4U�I�u�a�%f�)��o\�`�t�0����V��QH�����q0?m��_?����
*zLU�A�(�)$�u�q�{�	0&��`L��`��^��������zps0����=�J|�9��?�:�FN� ���2���6�I�C��Y�?Qn�����������������
�D���������}�,�Zr,�;���y��I������u^�imt��������hKb�Ax$���:v��y_�f0��Pg�C�������w���	�j��p���z"�*�B%y��P�]�E3����kDU�P^7�z����+��
�����j_Bg:[�����$��
��b�
R^�S4�%:��6`L�	0&��`�H��F���z�����wO��������/k�������/�</^�/�F��N�=��i�?��"�
J�oMy���I+�u�=���H�����T�$�7��8����*(+���Ev��h32�l����S]Sd?�,�r�6��L�������aC�@-Y�;xU9M��W�!�{7{�E�������x�	4&��7��g_���A�������QZ�D�����a���,��J�O��>��A{v��]���aOAU�Q_�,��|k�#������gR���&FML��0��@���!W���`
O����	0&��`L�	0��"��E�k��KK)����Mwe4K���w���i�$�}��������}nDQE����&m�����[����le������G�\q�kq���
��^*�lT7�O�R�F�X����O���?�E�3�#�im�����Y?R�G��F�Q�[�������pf�O�+7R��O?�����{/�F������^L�H�a&O_�h�*�,�
:��D���)����c��w!�i�7:�y�L,����r��X����A���L3�J�d7��u�.��!12S�`L�	0&��h}Xl}��^g����xs�fTQ����p�s����� �R�1�.��uD������)aCl���Y@3?��q�Kr3(�������_��.P�oM�0�w-� ��By��O�@���j����	���fJb�
#�P��]``�A���?Ad)��p	����Vz�<�/X�\�<��5],�)kW@�<&��p~�?*��8�D�_c�gClWhO������e�Gm|BX�Y��xn�u�3w��	oC"�>#yOc���:��m&��`L�	0&��C��'x���G�!�}���8���+;u�u��TF=W\����"1���Bx����yW��>Os	*B�a$*�(����U�sl�����l��zm��|��&������W��q�d�B���2������c^�?pl��s�D��7^j@���}��������e�~��_Qp�(���#�c[��6
��)q]��I�m,�����QRz����������b����|��[�����L2T!b���
�N�����M �	���6(���)�!`L�	0&��`�O����FM6�-g�q�������l�Cg�/�O!�����H����u�3����eo�������������A���=sK�'s�r�SZ�������$�H�����Ji7�������\�����vH�+su�ao:�d	�����"��_qX��n�3�����>�}��"��}�}�_HuE����S�q��/����
]o���#�b�1oH$��l����W2��`�j������;�
��,��_z�{=C���W��/U
��;���ps��=_��T��>�'��z��j�z��`L�	0&��h6Xl6��q'���X�vLY��(�	����w����}d^3�S..�C6	{�Z�O��?�a����O@�$���6�U\Y�e�����o���#�	��c/�}^i.b�U�,����L	]O������ET�6C.���r<g�>�\�ke��K������������P��{���q��{mU�����;r��(`������������>��n]mmrv��/�	�F���uN#�yICY�sd���%oBP���O�@��)�t�~F��`����Nw`>ph-p�X���!^������b���#�� d�lr��<�3��^|��!�
|������KY�[`��
cD*y��u<��L�	0&��`L�	43,6���=BYWW;���1zm�>�BC�f���������6��I$xZ�3/=�-:�v�����5}6���%������}v������Om��2!�	��d��F*�5�/��A���!�i��0I��T(5c�*)�G�&�O~���J��V�Tiw?,>y
���`��������0a�B�$%)e;��R���}z���c�x� Dvqml��VB�Uts��`�H�W�W2����+U�����:����'L�u�==�T����������He9��cW���Z���T����Q��Z	���`�l��$�\�:���$�#aM�Q�����`L�	0&��@�`�I�f�iy��?��k}8���?�v2z'&)Y�B��%���z<��O8�}:u��\D��,�V]�p]8
)���Jz �|jN������}&�]��H3�t��.��o�����~��"��jj� T��D�i���{]*>���:z�RS���w<����o�������`�x1���w5�������L�N���T����c!Q�=q�+���h'U�B�{�^������:������Cwr�[U�6E_��E��!0R�>+�.>K%2e�����3�=�
|f������V�L�	0&��`L�	�,���X��H	������.�Q�a7���^}]l�a���|�,�����
#;tlp��,���!^X	����w��g������4Z��b[�)����*�am778�`F*%
�z���E\��L�.f�>R�$\��A�2�j#�3�
�BWK��G��rQ)]+]�~��C����6���a��'�=t��nti+�K�r9yV��%�n���Z��1_��o�u>b[����=��n��z*<`L�	0&��`L���8M15�~EU���cyyNC�Z�S�������p1;9����N�&���9����;�����?wjz%  �(U�)��(*�������]�ZW����������+v��J�����d��=7���d��$��Lx����{�)���\�<��}���D�t����%C<8����6�G�=�W�6;��:�eKrJ�v��./��y`N1RM��?e�'�V�8'����v,�W������EJ���C*qM�9��g���#����0�|��zL��_��O<���������d�E$@$@$@$@$�����V4�K���m���O{v�$ �[lV^Ey >,�,�$+���Q��U�����WG$�����Ky���p���6�
�,���������kS3�j�*�Cf��$� �}�6	
(��v��ISdau�?��l��y�I�:)�?7�}O���u�<��������T�;T������(�ucG��Q|����$��o�^�;{��oM���-�Dj�.@f6b��������W���|���
	�	�	�	�	�	�"@0�6������q�.���7�GO\5l8�t���~~���T�<;�9�Km�d�����
�}�����_����cs���a�80��,#���U^\�oz��u�j7�+*j7��+`��ip���'��J��*�f��j�?���'��XTD|�2�=$��5n5��j��H����&�R��1w��;}�MH��y�|������fw��&�q��������E,<����z��G�����o%�$s�D�����V�@�������'�~����%H^�X�.��Y��qx��]���X	�i�����IO���4�H�H�H�H�H �	P���e��/��j����j[�EC�o��Q^�f:�a���JnT���#OA�����|�,zniT���������dM��@m����b�g���W��?�Z�@���d�,8����v�|;zm���������?��}�]g�_���FYx�����pU%L1�[�(���>�:���L������5���zVWoZ\��)���`��Le�$^{�wU%��-z�S����6��*��x`��T�
��To��L�
�Q1���n���d+o�k��c�}e��qY��48�����wH�y�3�O�F�	�	�	�	�@b��X��Y����T�\"�D��D[����$)D�o��}�(
�H7���V;q��{e����5�W��?��)�;����kV7���S�����`�A�p��"hxr���._o�����D������s�K]y<����p����N����L�kjU6�P�<���������{��,�����[���7iM��o�cjK�X���p�����8H��_��M���,��E	o���y��P�$���;��g)}���%"�I�~�5b��l���@��w9��� �������8�?E�>�/�^��     h	[�j�����-��P��Q����&~7�
��J�E�8^a���H/����9������i�N]+Pb��8=Xp�v��G�O��F��*�����h�Z3��d��e���o���*���������w�&��7��x4���6n���f���I�#��u�6�X}��%�n`c����^{N�(�,���2����}�Q����`�F&
��M~�L�%�K�	�z+�6|R�r�8%hb��Vb��'�%���_���W/�'�7*�LF���������G_�����I$��X��JA�������7e�H]�o�
�2d�	qw�l�Z�>e��h��������%��$�_�����	�	�	�	�	�����������e�D�����p����O���e�r����I1�D��?�
��A�&���
jk�*JL���dK���Z���$��*}j���liz���M�������s����c�
����"V�<��m������S�9N�3���X��[�����dN���%%�<?d���<�~�,��HD��c��<��-�'8j�E���r�w��x��
uk�k��t���?���o��wu>���$11��l����=o�M ?�&�d�M�(�z�p�b��8���a�:Wb�9E�R	hB�-X����P�i2O�O�������2v5��wE��r�6��})?��_��G��}���R������7`�V��}��0�)���q�����x����G     ��N�`�?a�xo=�h!���	VI�0��sQ,I>.�5��_�?��C���_$�m0�$������|J�kL�B�"���e��7i�"%n��W%��E$�dk�n��=���Qo[�%;#����%��	�����������R��������FlJ������s��"[!�8��BNr.���c�E�Z��g��*�l�F ��/[�oo��q�bGM������%�n�0����������h�.��m�to+g�u!0zDo&s�Fd�=�;���W.��	q�a�}�,�7~���l	V[�-{�5h��`l[�A��5���>�������5M�r$��\t�>k*��oJ�W�&����0�#��%B�G�Q����H�H�H�H�H�H�m�����7�~���l������������BP�$1�N�_��u��������A����O��
�n<1���>�����%���l�C_���TT�?U^[�2\�)������2ijtde|��������f�����Vi/&�9
�w�u���D$����n���>��;���i�����KI���������7��V�^5e�������l��n9�:H�k�{��[��w���+h������~�����[�nk�b����HE������c�@��"����R*F���	�	�	�	�	�	�
���$�`����b_U����+��������m/T���\�*>�KE�R�O%���/�T��������3~�T�
?�,�o
���W��b~i8TQ����������T)
#&��Z����3�4fW���]d�J��5��)�������/�ju�u���Hj�(�i�Ix���~�d$E��/�6��T>�7��e�k4���k��|���b���{B���e�Jy%���O�&�8����o�����ycB��V����ftj�c��<U6ue�O��~g�����?J��
�Z�R.M$@$@$@$@$@�����u��_�`yk�����7]6�6&��|4�9"����,?/��V��dO��y?H���@B��s���y�/��a���h�s��p���������~D}}��`���M�N&�G�����M���mooJ;����7����B�ohF�\�M��^s�nx%��u�2O~6����i�l�Wsg�j�����T|��C��'��0���P>�6h"N�
���=[�ORw�c%�pc%��H$@$@$@$@$@�K�`��o���wl�3>�fG�S����/�8�{��_&�r�rgU������t
���c��~��[���o������o�����l�
�U�._���z$Y�Kw)�6�e�����)�_8�V�d����V������[IZ��.��yL��"Y�UZ��VM��d��e�j�6�=�,���@�S�?d��7�3*�NE�����\�	C�M�����O��L��V��G7�$������Li�'d���z$@$@$@$@$@$!
����n�II���R�����V*s��I���0|8n=���x*]�xb�cu������W������w.�s�FC��~��������)Gt@��M�
��|�^��T�����������p�e����h����!K5��<.d��$��I�Lox����J����?����1�>I�-^�n�_�S��#� m���>U�������>���'��;���N��)pu;1Kp     �[����7�f��x��V1�Z�x����P�}�p��p��>X��[����c:�V�����sxo�;zl����5����Kp��7����L��M�WT�O�{��~</��RP��N������#{�Q�<{�]T_���p��6��V��I{"�R�G��3��������*[��K�����`���
�/e�s!l���(*��7��w�x�u�&���>���S	c����Y��*�s��@���9���C��y����7Pr�ve     8LPL��r�>����c}���N[sTV_VJ��]���*�_�e&��	��z������U�d��9�������K_������C~U���3���fXD��h>����&���3����P[{���}����'�eUq���zJ�.��{]�z���~i�zs.jv;�~�!l���������\����Yq�Oy�c���@�J���s�����H�	�|5�~�w\40[��#��\�?�A��I��yf�+�����'��l=����G/��9Q�����)/��^P>�!x%�xc��z�a3	�	�	�	�	�	$4
�	��*�.��,���X�	I���v�_�S��D�xQD�G��2?Y�1,K�-�)��:�TCA�N�>�����J��6���Q�OCv���0�9����q��3����xp�db�����$�#�)�ni={��e#�[Wd��
�lEo���#����dz��h��f'|0IR��w/��Q�q���F~������XJ� �����{��w�*>�����.��7,��-1��%��m&�m����s�D����n?&YK�T^r�&z��(z��_�g-v<����	�	�	�	�	�	��X�"�k�^[�3~��#;unq{�v�m�UB��%��^T����C���I<�6R��k8�KyM��3*4��gb�;;a����������[b�i�&���1�SG�X}F�<x���=�Mb<��3�����&�:3|��a������rg�����������(h���c1|!��w,p���&#�~P�D0G�W�'�;��{��A���o������r����T�]��2*>
�U$@$@$@$@$@$g"�m2�?���g�b���px<�`��G��/�`FU��@)���i��K?������`�t���U��F1�8�����p�Z�������?�H�N
�}�����k����$P��-�]<���T_�~���V��R�)����a�vF��fd3�G���_���"M<[��5�#	\�Z<��u��B���d4��������d������Qj���_�	SE\�$��Rc&���g�^y����������?y:�����#��rKo�>�N$@$@$@$@$��(&�C[��.�����������k��o�����L�j�Rg	���rRrQXq�Mm�
���<����P}f��p���
y-��I�1y��(����l|}�������;wb��w���AI"2�n�N��������*�����%�c���k��$6���W2����M�	ljt�����O���~�(!��;��E����u�R����B�j���y�,�uS���B%��w-���o5�>e�Qwf�B�����0��_�?�@$@$@$@$@$��	PL�����=��-f�
����in��Ix{���������� �����,��w��9i��r	o�Q^ss����w���=����
���]k��U��~
�^y�a�w��k�DZ�\v	���&�{���(X�3�����}"�".�Yw}_7��p��<�^]���n����^�-����+u��y+���� �����q2��58 i�[�j{���A�xQ�1lTuG�I({�TJ�0�����~�����I�H�H�H�H�H .	P���R�(��c�������b��_���:���j��C������8�����_�M�)V��`�
����B�X�$�{�^w3:7��2���9v�������+�k.%�z$'�����/����O���P�H��jE�+.��Y� )7#~��jSd���I�b�:Y^��������4C`%p�joD���i��!�������E��>If�����x��*���}�v�������������D�����n�}g*U�;�4{<��W%�p�<�j^&�n4_      �HP��R+���x���K���-�/q���T����4��H��6��0�R�=%�M���rk��*��O�u�p,&�������f\��x�F\�E�7H�>/
V��-3��W<([�����*T�`�{���R;u�Q�_����E��!�|-]��u^��i��Zs~��9A��7}Q��SNdp��F��U�7�v`%rg�$�*9���i��&�w�R�����M�"67cRe�3o�.x�rIl��V��<��p(	�	�	�	�	�	�@�j��w���[�/����k{(5��T�3��
��|������2�)(r4q��O�����9g�~`EF����A�7_��+/�'g����V�N'v���/�ZD�U\���p����1���t���N'?=�
V��G��J/?�\AS�SS���Kp�U�����xg��%��%����������K�}����fl��PD4�,�;�n�%0�G��������������
���P1�Rx��A�^-����2���	�	�	�	�	�	�"@��x9��u[�)�&xMP}����j_W���<�6<����a�@�dBm���Y��1w����J`�fM����`��-��7���W���s���#�wWeN�$a�%���c$��7��l|�m��������zdg��Ga�-�F��p1*��3N<�d��3�����C�`�s����e��?�O�,��[u����]��\tG�lk7�(�����N���6�����N����(?�f�$!�����%3`�6���l��s��)	���|g��:��{a����o��VYb������c���{U���n��d���rYt�l$@$@$@$@$@$@Q!P�@De:N-V���u�fg&��kB	�.Q[����y�l�=��pj��p��7�y-\�(y�E�6�2�k���#3��������/:g����6�
o?w��������&b��3����"�}��ew������2��)���n���m%��c���������+��b����
��s��;�WY���X�N|�Iy���&�\9��>K24�VT�������+��#�j��7���5�����Ep�u�n���a���K���'��>	�3!/�V��^�$���[����":k���&����+�$U):�oP�������$,$@$@$@$@$@$@�K�`�>��J�~�R*������m~���Y���E�0!����>�I��v��6+�]6�
]�Sx{����N�q\�"�����I��
����|��x#������K�y<�|x�d�"o�l���|��}�aMKCzr2���;���0����*[}Y$oKZG�
j	�z�K�T���\(�4�����!��B���XD�3�jO�2
����J�!��s`r4����#G%��{TG�-[�����v����@���R*�]b!     �����l)�!D���j�.�i_���H��)�O/<�S�K�T_j��O���4��{�d���^���z�>����������a�m�*�F�/k�������t���52�_�s��~)GT�TK�����=��0
�
ju�I�l?�3��V�^e��A��kLc�Z��ktut��$�x�iG��*mu���w~U���K	n�Q����]9}��>N<#����L��L�k���H�H�H�H�H���V|r��	��O?���|���+�pJDzb��f[e�����k��	8�m��]F��_$�I�������Bb��~�9�;��?�u/��o��7DR!��3���*6�!F����UkP�jU�>�]���*����4R�4�9h^7��0��F�I��I��V�d�P"_�� �Z�$�4OjG�L|��v�Y��d�l�9%	�	�	�	�	�	�@k����O����_Q�[0�����-�]23�m��e�opV�fO� ���+q�^��dn��a</����>�E��5�4F�3��u�ix�����&�w�X-"V2L���s
Q�|��0����� ���Cmm��Y�(B5S����=$��P�L�u���X�>���F�����g�����pv��c������X�s�	�	�	�	�	�	�!
���PJkm���OUq��k��j'�h���<����F�v�������7�����~��W.�{���8�=J���u��;x4�������qM�'�b]��1��'�����8k�����I�!q�-U�o�>SP$1�{~����a��������u�_lS�����.1:3����T���3L�����'���E��u� ��,.C$@$@$@$@$@$��(�"|�x�9�-89��o���(���IJj�;j���f;���=������_����8��i�aP�E7��6��9����u%�t����V��v��7��w�������?c��_��x�#k�@��zt8vD<��6���)?�S�����������>�$�y��!�\0��"��>����Uug��z���� ����r�ol�/�      �6N�`+>�nYYX}`�,H�>��B����D2���rz�30��(L�{����q��nT|"����3���bT�����M�����Q��?-7��QP���~�>��|;������Y�+B���c�_��s3}�������*[m��(��r;N��x��T�#n�o�i	�	�	�	�	�	�@�hkzQ��l��s��H�kp��5"�U����j����B����~){��	��C�=���$z���q}���c�;��]V�������DL��8{�SY7Cn�d���y%v}3�Z��#^�����n��6e{��UZ��n]�^������M��PV����Q�,��9��3q�+P��m��������IBS��W��I�H�H�H�H�H�H�6
����������iyT�8T���*��ttJ��g>���L�}J�o���A���&3��7��{�[���#;�������Nj�N8�|����� i����
����6�v�dOO�#�p*��!����������%Xw��y���y]��mj0n`�-WW��O����9�P1�"X$Q��,����U�mj@IDATz���[�3�	�	�	�	�	�	�e[��6A�3#_,6�P��-�%��w��+q����F�+~k^����k`�Z�u�����Bt9�dt;�T�>���Xg�e�?Y���LV+l"�
��vq�(��U��Zj��a^��o`�6����o�-b�����9��z��a�IC�N�������!�y/G������`W�c�<?�	�	�	�	�	�	��A��A���e
e+
m�V����`y���B[Y�YD��.x=s{F6 �^_^|J6m��V�~�G����o@������'���j[|u�$%�9���md���������"�!��^$�7�o$�E�������Mpw���w����p�!      �&��dt�8w��,-i�$�^z;m����XLV��?G���+�
��Z�5��HG��K2'a\�	z�?{�c������-[k����7��?H<�R|t�9A�Z��d��o��*K��E5g2D�3Jk�y��
��f���x8��s�����hh(��	�	�	�	�	�	�@T	P�*��'{yEMF�p��%��Y����G��L���xz�S�9���OY`F��&XdG�>�=U*�PZ�9b(�=�?��f;���Ck�b��o�YX���)Gtw��^7���dk��Q���"�?��1�[�]��bq!s@?{�����$ki�X�Hk�%���y��g���Ir�`*�o���D$�����������	�n�.��X!      ����A��Z�
�w�j����	'�[NR�6�k�N�6���'��o=��y���[L��������;4l�(q�����B�Z�|w��
[?��g��;�[���.����
�$6^SJ���{�EX���5�=l������z�3
����1�e[t)���~y>{**^��E�z�1�&����e�~���t�Rs�U��]��A�h-�U��N���l��K�6���H�H�H�H�H�H���@�M����@��_^���}��~}T���l��A}�������h�X<"h�O��*��rn�(y�J�	��N��������_��������4�t������B^��X�v6���[����Q�i#T�-+K<
[�Im[��Q�����y���2n�����*KNN��f����J$��X��2Ni?<������]�z6m�'�}v4G�Oy��lw�
�����I���W{M_jx�	�I�)��hp����J}�3c�o�sW}��%�i��u��?>��������H���{N�9�n%��S�UJJ
�^o�}�Zj6W��3)�a���&��#�&~U_teMH�����]x|�����\p�@�����i�0-\v���0��SD:1U�+����?����i���c�2��G�������J	9�����;1���Q���~i���kw���c��;:;GV�\�Y�� ����)��
�����.���t��d7*7,�������xh�_�c���a��Jv��;���i��?u!"�����������	���Z�y+E!�~O�	�+��x^��������[���?"�m����������B����e��#���~>��%%%���.W�.��>������<^KZZ���W�i	�@C����
����F�Ej�>u���^L�3��
n+��7�3�"���^�#��4)�|u����A�f��v��$#�cGu����������[���/����F[v6�<i��y�8����Gs'��8O�[hC�%n_����\���|���������v�?k��|��kp�Zk~��N(:�5x3E�����P��	�	�	�	�	�	�@\h�
F�����U=%�G��j�J�?����-r���+���;���8������/����K��,^5�����v��P�}LV����q��o�m�bNO�Q�\���C�s����z���Rgqw^��O]�4�{���`�X���?pv�oz�:�
��n1��Sk�~�����/4���H�H�H�H�H�H��
�1~�Gfdb�������K2����x����V��w�jk���/������]N9�6b�ko�����gM�X�[����x�f��Gz��j�������O��~�RG��[�v�x���!��	4��y��8��~���x>,������u�j�C�SG�=[����;�V��$@$@$@$@$@$@$?(��Y���=�h����b����X�{<N�>�o�+?���c]y����,���du��bo���o�}/:_g�-����s�������z�t��M��Z��p���vI0r��j����\+�z�:�8xS�^��+�MC��������v����&?�^d||#l[�o?������E������1������>�7Ce      8LP����l�����2]��T���r��w�cFG�/���.|�������C�>�I�N��Z�
����Rx[!��	�:�m�q�%~����y��W7�t������Nz
��gW1�zD���UW��p���$���&���	w��w9���l
��?���o�:�
�'��RKq^      Hc��*D��%�V���?�6���v�[m��^����p���B-�s7�2��}��BT�����]`	���!��g����f�
G�~�l�w/��^}�$;Jr�<����#�v�nh�f];r���{�%��������g��K���$��ysL�h+�z%���sK��72���,�p��$I?.�e�JhN�����|&G!r�8S��~������
5��E�8tt����Wy��	�	�	�	�	�	�	�`~��7�j>������vC���J��i$��Rd�f}�Hb�-��*`����<�}�t�4p��������D�$�0��{��1��
E[�`��^4�����p���`����O����J���Tl6L��C�33�2][�$��)"��o'm���z���QM��JE���[S="�V-����9m����t���4a{zs��X      Hc��>���������������{M�#�>^g���,y�o����OC�K/�����8
������z��y������[~����vq���?>P������{�������gJI�9�����	�Z���)��a���2?�9L��_4�+����p8��.l��_�3�	�	�	�	�	�	�@&@0��Cjh���R���]��_�U���3/x%�r*��[�N��:�������q�_GR�vA`���!�P�E��c����R����=s�7vH�����O<u0���(�=�9��I�>�l��Y��n?�Uo�V��{Zj2&��j�m5?u�����6%�%S�����9	�	�	�	�	�	�	n(���_1lf|��zE/�X��R��������Mz���b��C�,���/�E��V�?{n.�����#^|�,���v9u"����������c����|�$��C��S�1�>�?�a<��"`&J�n���o$��}���K����t��|I+_���V������43>�
�%W�Us���qM��2&pQC�l�$��Tb�9�M
lf�H�H�H�H�H�H�H��(6\S�%Y�Xt������}%�~���~�c���f���c.�
�n
2}��c�|�$�����,��g�8�y���t���Hi�??�O8�����%�GM������r��n���{�������0�_3`IJj�����I������u�F(�,y�P~��ko%��������]������K �O��+������x��%P����p�?�����7�Ck���I�H�H�H�H�H�H����G�%Ys���T\=�5������sg��C��g�b����p*�s=Y<+D���d'q��`�W� w��"���sW	T�`���o�7���V;��h��M�����u^{&��:?�e����HY�
�J%��(xUE��`��0���-�)�"���#�?��X6���ys��F�����H�H�H�H�H�H�H�9(6�^��-��[>�Q��`��	S�����a�L��+X�*X�S)�e��*��C�ZE3���m��p`����7&k��kS>�(a�?�������,��~�,�A�v-E��_���l�6{���e��H]�L�a)�r��s+�b��*='������b��	�	�	�	�	�	�	���#�b�g(�-�-YR�)����[p����E<��b��|*�����O���.^IR�k7�\|!~����^b����5�a�1�8o?zzO?�����r-��$�I��h�`���_��5��g�O���(q����T�c0u�����X��\����{F������lNL$@$@$@$@$@$@� ���+]sSs�1
�����{�M����:�;����A�k����g��~j�����[~[��7�L���m�g�z��H��U2�bi+%�0��8��������&^S��c�$���.4��>�~��o�@O?����sd[{�a���~$@$@$@$@$@$@$@1"@0F��e������=~q�7��k�cW�.d'g�������;�����#	@TY������
��z.�	^�,\�V���������<���s�$������y����P1�6�QX�xcl�����0
����~��`��{c;	�	�	�	�	�	�	�ec�t���m��_]3�^��%�6j���;t�/S��j"�������)��$��6�N��`��sB�m�}��Z��Io�����Zn�����K��~_R�F�!��C�e�8���U���~lHC�S��N�K���wj�S��Q7:z���)��U���<�	�	�	�	�	�	�	�=
�1~D�1��~'�U�~=>^���md��p��"���x���7fa�+���{U����|\2�~�����.��b�>�dd����))����N=�5��hm��i��e�2�e�����<�]��-�>m5��Xz�#5���j[�*2��6�3�[w]��Pv��pyLJ�3Y�������������|5b����������%H��5�&��7��V�H�H�H�H�H�H�H���&_��}�F���QVV�v�(���_=;"[{�����Q��:��_\p�x�Uo�o:kj�8���1�44A+]�:u2�}����z���6"��D��N���iG���`����qY���
����E�BOzg}[n�q��;�$[�SDT�`e�3�xA�}y!z,����"�����mb�d��'����o����������t���&w����*��a6�����%�	����gdX���
�
�����c|��l�!NI���c���j�"hz6^S�������EC�S�Z�Av��k��m���b�8����p ����3�zS"��L������������&������}�7�&e�|�
��\��D0��w�VQ��W&E���t/r_��J���W���/��0���p���S��������nv$      H\c���X�YD+>2����5����u�,(�ww�]��ygDf�*`v��%��*�N�h7X�^t�����!���.1�����3�?��Y�^.��,�V����[�h�mA�'7�\�
����uJlK^��_�K�~��=����o��d��H����::;A��tw�������Y_��Hc       �6K�`�md��}#�����%������Q�����?U,������@���i� +�$��Lpu]��S��2"F�|�+^���{���:����o�u��"�9��/��~��0+�S���`�~F�hkD?Mb���D"8���5=�!      8P��C���(w��U��
Ke������#��#`IJ�����k�x�+(� C<5Q�<^��2Df�FY���������>�w���{~��������9���[OF�s/D��&�x'��t�$�p��]��7���|���~!Os���A�C��Ct��D��s�6�f��xT{S�9��H�H�H�H�H�H�H��#@�>2-�>{��ag]|��a�v(��n�MO�Q�s&�~	������m&.�z.��]{m�����+n�'��)q�qHZ)	M�>�7_sW����$�z���L�K��'��N#�;.�����l���;���'�b�/,"���B\��h�
�>5_���P!�s����e\;t����</�h��yH�H�H�H�H�H�H���>�E�4�����k�z�����	*��Q~z�	��q�qZ���2��w��{��$�����@�����r�����'��&�mf8;�$�?������]~!O-���}�����p��^s�X��������J,>}��^}��I���-�'��k����Mk`_����	����d�e!       �h�
��q���1w�7!{w����O{&������A���|NWP[,OzN?C��&���M�����O7�|hSUR
����d����S�7�}��02}�Cu�<M�V+�O��e/�������U�~^�]b
�����������~5����H�H�H�H�H�H�H��L�`��5~��.#1 g ������y7-��7���C���_4���$�<2��7��J�	���G<���#m��`��94w%\���h�sH�-����h�u�,��J�a.���O�.�c���$
�X�8��`�Z�|-�����>cmUW�C�O���c�H�H�H�H�H�H�H�bK��%��.��p�L8��ms�fdK���G4����0�?��Ic�3�j���|mV3<(<��p�������*��S��f��#l[��'��x�Ya��}�������gd~v+4i���g*�%[`o��(���1�m�R������P>�b$m�B�Cw�j�WU�Q��$������0�Jd�o'���-��XI�H�H�H�H�H�H�H�%	PlI���m3��?�=W�7�y�e�~�?�;�@���LG�C��U��/������"��D�WwK�;��[]4G1L�6���K���?�fT�>I>��?��y��+�o�\�,I����h�{
�v,��m��!��"
��	O��L)�qy�9OH�H�H�H�H�H�H�H��	Pl�'Pk�]s��`�j�������X�kE��8pe{���cQ�q���[��'�=R^=���4W���^���Py����TEm�M_��P��W}U	}��D�j��Y�(���le�����!F���1;��~�����(���	�	�	�	�	�	�	�	4��f���u3_�/�{��Jhv;|��M	���}zT�+Yy����Fbk?r8��}&:;IYY�*��m	���A����}�����WK��zE:�m7}��Ac����_�
j<��&�5�u�����'{���d���\�H�H�H�H�H�H�H��@�` 6v
�d�U}>�y#��s�����[����ki��};'��t&�d��9Y.|�e���a�������QRR����,C�;J|��z_���OFg���'�lU7K|����_���a�������l�_����#	�	�	�	�	�	�	�	$$
������(X��"ZS�q�3��oA����Y3t`�.��������y��n0��O�D�����B��/�|hSh���zD&�E���(�O�����g�aH��U�����	������	�	�	�	�	�	�	�@���'��r.[W�w������������\��W��Xs�K�=%3o�[��$|�G���G�/.��&�(aP.��M��E�������O����
	�	�	�	�	�	�	�	�5c�De��f��%��t���Y1����������3�}�� ���/���S��*���$�H�eVP�@c5���������]F�2w |����$#IG�+�$@$@$@$@$@$@$@$��	P��#.������/���p�s��r���n�[���������yJd�5�n��E=��Y����������X����a.�
����������#	�	�	�	�	�	�	�	v(������������-[rF�I�����e�
d�0Fb�y��3w��}k�D�@!/���VF>�!�)��������
���r����Q2���k�c.V]XH�H�H�H�H�H�H�H��&@0��?�}^�`4��Xp���`�Z��j�bd���Fcm��h���!����*����%xS��r����X%W:���W�i	�	�	�	�	�	�	�	�*
�1����s���OI��o���������+���?����b����Xv.A�W��qF�:����a���
���~��?�9Y�+g���o�6oj{��p���G        �0(�����=�+���gM���A�^0�|X�*nw���QN�|��ssa_�12�|	����09�P1�b��`_�>R��]{�~�����
nS*,�2��e�G�)���Y!       h>
��g��
�o�#���^\z�}I���x���\�"`��Sp��V��,E���B�N��I�^�zW��������������~����Vv        h
������?�x���.�PyA$���.5����.��kG��#1f�S0��FqW��GB�8��h�x�%�R}�?�x5+*����?M<�	�	�	�	�	�	�	�	��@�������}
n���X�$3������0��K`�p-|�������7��b��X�(��T�{�NE���T	�	�	�	�	�	�	�	�@+�C��^y#�j�v��c�=�VJ@��������'��IO�}�q��X������1��_���b_I�qH�_	g�Q(������F        ��!@0��.[r���*���*�O������/n��l�{���BN�FcMuT1�<�T]����V}��(���H�H�H�H�H�H�H�H��C�`t8F4�Q7]�=������SN.�c�]�ub
��WK#��3:����f+v,I���{��?��8	�	�	�	�	�	�	�	�@LP�	��E�{���[o���D ����0��?/Lp��%%%��]�+$@$@$@$@$@$@$@$@�D�`�����.������i����U��_EV?�����G_����&3�[�*.J$@$@$@$@$@$@$@$m�M4��4g)R�{Z���!�yD0E��b�1�+����y�E"�YPj\��H�H�H�H�H�H�H�H����G�v ���a)����_�����}�@��7���7�w��H�H�H�H�H�H�H�H�Z���O^�b��?C�3����������Q~�?�&I�H�H�H�H�H�H�H�Sc�������n������;��>����\���w��n�	�	�	�	�	�	�	�	�@���'����zJ�S�����f�����i���,$@$@$@$@$@$@$@$@$�
�
������~��*I�A�{�b'        ��	Pl�OT�z�Y!�S�~FQ���l2Ny$        �f��,|�\~��HY��?n�1Z�n\	XS�&I�H�H�H�H�H�H�H�H *(Fc������/�s�T�������|0{�	�	�	�	�	�	�	�	�@#	Pl$�ht/���hL�9H�H�H�H�H�H�H�H�H ,S��@$@$@$@$@$@$@$@$@$��(&����$@$@$@$@$@$@$@$@$������H�H�H�H�H�H�H�H����}t4�H�H�H�H�H�H�H�H����{�	�	�	�	�	�	�	�	�@��������	�	�	�	�	�	�	�	�@x�3b        HX���p        O�`xF�A$@$@$@$@$@$@$@$@	K�`�>:N$@$@$@$@$@$@$@$@�	P��=H�H�H�H�H�H�H�H�H a	PL�GG�I�H�H�H�H�H�H�H�H <
���	�	�	�	�	�	�	�	�	$,
�	��h8	�	�	�	�	�	�	�	�	�'@0<#�         ��%@0a
'        ��(�g�$@$@$@$@$@$@$@$@$���Z��|	���	�	�	�	�	�	�	�	�@<����mX�~=^�u,^��<y2������xN$@$@$@$@$@$@$@$@$�@�yVVV�������1c�.����+�9sf�[�e         �������^Caa!���.��v��������Y�p�E�f�����	�	�	�	�	�	�	�	�	�C �<�,Y�Q�F���a��1cPTT��k�M<�	�	�	�	�	�	�	�	�	D@ ��]�v!///��v�������A�<!        h�@�m.++�������:t���c����;����C1l�0�5V�O@m�6�76����f3���a�Z[r�&�	�y�K|�4
�����9��;� �G���n��s2,��e����z����J,�M���@�~��	�@,	��h�X��1����Tq���f���0DU7��;��ld�l�1��xN�1&�r�&0��ql�$�32�1Y��4��U���t�zV|^1E�������/f�����+f�������c�'�@ h$��srrPRRt����yJJ���{��z��P\\\g��#+M"���N�M"�A������#��s�FP��a<�F
d��P�������Q�
���wVD���Sjj���w+.GX#�n�E7���P^^��76����?���C�rD<�@-@ �b������ �V���;wj�		�	�	�	�	�	�	�	�	�@��N1bT&`���(�/���������#	�	�	�	�	�	�	�	�	�@4��������1}�t�y�����k�q�F�{�����+q�y���Cm}�g��F�y��Ig?qN�������1h� �m�,�O��V�?#e�
=�M]~���a�an%����X�|9T��N8!q�>�-���8��Y;v,:t��8F���%�����	�=q�]�v����?���{�A��3a��s�9��W�����^<���	(0//�n�w��#��P��^x��������\��W����{q�i�w�[$��P��!C��w��-��H�H��@�	��������G�����
8��H�H�H�H�H�H�H�H�H���np�o�#H��&�v�Z}�Gvv��
�wOQ$��J�Z�
��
�����H`��=z��n��	�@	�\�=z���F*�" �6F�`{����O@�fT1y*�����F�L@��m(4D���h��,i	T�6����j45��w&�ym�@+P����p^�H~�c��0qI  �(���(@�$�s���s�=Wg������������1s�L��7EEE:t(���? 33�?.Z}��B	J`��������9s����O?��|�	~�����7�t��>��E2��&�$��Hq�]w!++KO�hs$�c����������###�'O�W\8
��'hR��@����/��K/a��HII��#�������[�;�{MM�>~�X! HH
�sSB��&��"��*o��N:)���cG��(�P��u�w�������q���ej�V����@�?>������m���O<��.�����F�~��wJe�7J���H�1������������}�]HS�}�UVV��������1C�^y����	�����G�g.��?������`u�
7`��u���;�r�t�#y'"�^�V�x�I�H�H�"$ ��,$@�H���n�=����Z ��o��1>6�}�l��;��}�����>�X!�#PZZ�{��������.���^�.��b�O��K/��'����h�G��h�$�$����K.�I�^�g��{��G���{�����M�4�'���d��M�2�'���E��VH �	��w��M�>�'��~+�k]�[�l���;�{MM�>~CY! HX��P(e7h)�6mB�>}�������~��-�5��{��������J5F��VH �(����"������'s�����m��tg"������E2O�<!�8&��G�]�v��:u
ii���%K�@}���T��
i�Y��>!
d#	���S���~�1���eee�����H����'���  �&`�&��0����|b������[uC�%o
s�1��v��c.�������������=qh
}���������7��c���;������kJ�P�������		����j4�e>�����U��*j�*���F}2�AqN`��qu,T1�f��B]�N��n}����"����O�$%un�
$@$@qI��q�Xh��B@�
���r�J�|�����+u1B	��5�/����u���C�����Sg6�@�HKKk03�J���J>X�{��?����(�ym`���@C������
�=V��S��*��c���O�?H �(�>�\p��q��W�%
��|�E�O����$@$@��`=`�L� ������&L��|j[��i����/Bb-�b� ����m#F��h���=s
h
��B���V�v(O�h�G���6�$��"�������zo�6��q��>�XI Q�^�Z��=p�@\u�U~����|�E���(VH�H����~|4>�	t��g�yf�m�����
�
u^RR�G�����T�=Z}�,�h#��yU$YH���z���E2O�<!�&���1�]LII��>�{��>	�����.]�����?'3�;��Z������L$@m�������J��������EJW������7����uS��������OH�
P�H�b�3nM�G���P"D���H�1����@��cF�Z�~�w�s��z�zo�����G�w?��~���c��x��'���@����|�E�O�]��	�	$.
����hy 0{�l�p�
zvR�v�G��?������M�+++�l�2�v����6lXT��`���p@mU\�hQ����h�G���AF���@��c*�i�?d-^�X<������zo��'�Q������-[t�Oe�W�^3d���|�E��a�$@$@�M�`b??Z��&N������~Z��������w���w��G:3f�������34�������`EM�(F{��5��������"FD��JT�!Q��"FQ��`�(vE��Db����sa���7e�'����/$;s���;��e��-iii�t�R�Zbxxx���sN��@��C���S����III���3�������A���{L���n����W����-[d��q�^OyU&�F����	��t6�Mt�?
���������s�ZO>�}�iEyU��i ��_�����<NA�<�H+V����TS������
8��A������;wD't�����=���We��xE�_���%!!A�;�r��(..Nv��iz"������#%""�Q.�>G����(����	$88X�����bO�������������.~5}�t��fU�We��xE����%***����=[z��m������{-��d�` ���@�yT44�4��b������W�P{eW���e���s���L�9���K|���!��<��2o���J�*9��u�-�����>�*��g�����*����@@� �@@@@X�9��rk � � � ��=� � � � �@����!� � � � @�� � � � �,@0�.�� � � � �@� � P�~��)��/����o � �@�	�g�!� ���;v��Y�f�����<�� � ���@�F�@@@@��(��gr" �����'$##C"##e������;����4h�������k�.�}���o������[J�.m�?~,����V�ZIXX���������"����|���y�T�RE�u��������/�z���<yR��/
6���K�b�\���F-���.���7���Y3��/_�,����>}�H�
��S�N����A�������o�����K����D�����gE4�?^�-*QQQd��� � ����H � ���`�-88�6y�d�����kWsO����N�:&�\�r6{P�lk��K�L{`���3��!..���������u��GGG�������6{���k�-[�l��_����7GqO���/\�`���u�kmL�6��y���e��l��U��X��V�P!�=�g��Q�&Ml�@�);|�pG������w�� � ��.�`��r ���|�R�l�"			�u�V�7o���l2l�0y���l������^x)))�'��r�����QW�vm9r�������y������}��v�~�y�����7��G�$))I>|� ���cz���r�O��^���o��1�\�n���Z���Kn��!����u������?R�^=��/@@A�` <E�@�����[.\(���3A�:�������2{�l:t�.\X��������W�d��U�N���x�B���k��>Z��];���V�a�5k��a��q}=s��V;i�$9r��n�R��B�;w���O����M���t���Z�2q�D34�j���l�2s��+W<��r � ���@�~|4@��	�~��>T���1�9�lkPN����I��t��a��s�i���3g�������w��EGyS8�_��N�����4y��q��P������x���m[���j�2����� � �@A X�2�� P t�s��p��z��r���=�tM���7=��a�G������}.A��<x��"���_��
�eum=n%o�h������sN%K�4�:��� � �@A X�2�� P 4`����)#:V��sO�������
����=z�0Cs5_�m��1+kP�|�������n����t��V�����K���7m���{rO�?v�r���g � �Y�������@Z�n�����y��o����	���8�u�:$:�K�.��5i��E44`�S��2l�"�I�����O�>o�h+5@��������#� � ��@� ���5Lw��%&��|��/6����"""D{��!�@�l����/_^�/_n��������g��f���5k\��{�-�c��I�������6V�X�����"%V�}��\�v��f�;�*����c���/9� � �~)�������h4 ��@���E|�w����H;v�Y	x��M&o���2z�hVPP�������W�\9i���9�Ch;u�d��^xC�'�kIDAT(�-5m�T��#6l0���_�~-����A���X���7m�Q���n��,B2`�8p�<z�HL0Q�(�m�95�]��w�2eJ�s'�m��� � ��)@����� ���@rr�,\�PV�\)���7W

�9s���x�H�s�a��9�����0`
$�����I���]q7&&F���kN����W���S�:������o�Q�F��={��-g��!� ���8.��F�v�d��!���":D�E�s1�0 � �������������@<��{�sO
�me^���������a�|t,:W�M322���W&E�q��_m���>}�G�����@@��>� h � � � ��!�" ��J� � � � ���@y4@@@@�� ���� � � � �����A3@@@@����J� � � � ���@y4@@@@�� ���� � � � ����y���EAZ%IEND�B`�
#19Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#18)
Re: [PATCH] Covering SPGiST index

вт, 17 нояб. 2020 г. в 11:36, Pavel Borisov <pashkin.elfe@gmail.com>:

I've started to review this, and I've got to say that I really disagree

with this choice:

+ * If there are INCLUDE columns, they are stored after a key value, each
+ * starting from its own typalign boundary. Unlike IndexTuple, first
INCLUDE
+ * value does not need to start from MAXALIGN boundary, so SPGiST uses
private
+ * routines to access them.

Tom, I took a stab at making the code for tuple creation/decomposition

more optimal. Now I see several options for this:
1. Included values can be added after key value as a whole index tuple. Pro
of this: it reuses existing code perfectly. Con is that it will introduce
extra (empty) index tuple header.
2. Existing state: pro is that in my opinion, it has the least possible
gaps. The con is the need to duplicate much of the existing code with some
modification. Frankly I don't like this duplication very much even if it is
only a private spgist code.
2A. Existing state can be shifted into fewer changes in index_form_tuple
and index_deform_tuple if I shift the null mask after the tuple header and
before the key value (SpGistTupleHeader+nullmask chunk will be maxaligned).
This is what I proposed in the previous answer. I tried to work on this
variant but it will need to duplicate index_form_tuple and
index_deform_tuple code into private version. The reason is that spgist
tuple has its own header of different size and in my understanding, it can
not be incorporated using index_form_tuple.
3. I can split index_form_tuple into two parts: a) header adding and size
calculation, b) filling attributes. External (a), which could be
constructed differently for SpGist, and internal (b) being universal.
3A. I can make index_form_tuple accept pointer as an argument to create
tuple in already palloced memory area (with the shift to its start). So
external caller will be able to incorporate headers after calling
index_form_tuple routine.

Maybe there is some other way I don't imagine yet. Which way do you think
for me better to follow? What is your advice?

I also find it unacceptable that you stuck a tuple descriptor pointer into

the rd_amcache structure. The relcache only supports that being a flat
blob of memory. I see that you tried to hack around that by having
spgGetCache reconstruct a new tupdesc every time through, but (a) that's
actually worse than having no cache at all, and (b) spgGetCache doesn't
really know much about the longevity of the memory context it's called in.
This could easily lead to dangling tuple pointers, serious memory bloat
from repeated tupdesc construction, or quite possibly both. Safer would
be to build the tupdesc during initSpGistState(), or maybe just make it
on-demand. In view of the previous point, I'm also wondering if there's
any way to get the relcache's regular rd_att tupdesc to be useful here,
so we don't have to build one during scans at all.

I see that FormData_pg_attribute's inside TupleDescData are situated in a

single memory chunk. If I add it at the ending of allocated SpGistCache as
a copy of this chunk (using memcpy), not a pointer to it as it is now, will
it be safe for use?
Or maybe it would still bel better to initialize tuple descriptor any
time initSpGistState called without trying to cache it?

What will you advise?

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

#20Tom Lane
tgl@sss.pgh.pa.us
In reply to: Pavel Borisov (#18)
Re: [PATCH] Covering SPGiST index

Pavel Borisov <pashkin.elfe@gmail.com> writes:

The way that seems acceptable to me is to add (optional) nulls mask into
the end of existing style SpGistLeafTuple header and use indextuple
routines to attach attributes after it. In this case, we can reduce the
amount of code at the cost of adding one extra MAXALIGN size to the overall
tuple size on 32-bit arch as now tuple header size of 12 bit already fits 3
MAXALIGNS (on 64 bit the header now is shorter than 2 maxaligns (12 bytes
of 16) and nulls mask will be free of cost). If you mean this I try to make
changes soon. What do you think of it?

Yeah, that was pretty much the same conclusion I came to. For
INDEX_MAX_KEYS values up to 32, the nulls bitmap will fit into what's
now padding space on 64-bit machines. For backwards compatibility,
we'd have to be careful that the code knows there's no nulls bitmap in
an index without included columns, so I'm not sure how messy that will
be. But it's worth trying that way to see how it comes out.

regards, tom lane

#21Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Tom Lane (#20)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

The way that seems acceptable to me is to add (optional) nulls mask into
the end of existing style SpGistLeafTuple header and use indextuple
routines to attach attributes after it. In this case, we can reduce the
amount of code at the cost of adding one extra MAXALIGN size to the

overall

tuple size on 32-bit arch as now tuple header size of 12 bit already

fits 3

MAXALIGNS (on 64 bit the header now is shorter than 2 maxaligns (12 bytes
of 16) and nulls mask will be free of cost). If you mean this I try to

make

changes soon. What do you think of it?

Yeah, that was pretty much the same conclusion I came to. For
INDEX_MAX_KEYS values up to 32, the nulls bitmap will fit into what's
now padding space on 64-bit machines. For backwards compatibility,
we'd have to be careful that the code knows there's no nulls bitmap in
an index without included columns, so I'm not sure how messy that will
be. But it's worth trying that way to see how it comes out.

I made a refactoring of the patch code according to the discussion:
1. Changed a leaf tuple format to: header - (optional) bitmask - key value
- (optional) INCLUDE values
2. Re-use existing code of heap_fill_tuple() to fill data part of a leaf
tuple
3. Splitted index_deform_tuple() into two portions: (a) bigger 'inner' one
- index_deform_anyheader_tuple() - to make processing of index-like tuples
(now IndexTuple and SpGistLeafTuple) working independent from type of tuple
header. (b) a small 'outer' index_deform_tuple() and spgDeformLeafTuple()
to make all header-specific processing and then call the inner (a)
4. Inserted a tuple descriptor into the SpGistCache chunk of memory. So
cleaning the cached chunk will also invalidate the tuple descriptor and not
make it dangling or leaked. This also allows not to build it every time
unless the cache is invalidated.
5. Corrected amroutine->amcaninclude according to new upstream fix.
6. Returned big chunks that were shifted in spgist_private.h to their
initial places where possible and made other cosmetic changes to improve
the patch.

PFA v.11 of the patch.
Do you think the proposed changes are in the right direction?

Thank you!
--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v11-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchapplication/octet-stream; name=v11-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchDownload
From 17c6d7d4c474dfae2c7a0776b0962fc8a4a3ec3c Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 26 Nov 2020 21:15:01 +0400
Subject: [PATCH v11] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE columns for SPGiST index is intended to increase the speed of queries by making scans index-only likewise
in btree and GiST index. These columns are added only to leaf tuples and they are not used in index tree search but they
can be fetched during index scan.

The other point of INCLUDE columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in certain
cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster, update etc. Also, any data types without SP-GiST supported opclasses can be included.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/common/indextuple.c        |  45 ++--
 src/backend/access/spgist/README              |  17 ++
 src/backend/access/spgist/spgdoinsert.c       | 194 +++++++++++------
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  88 ++++++--
 src/backend/access/spgist/spgutils.c          | 201 ++++++++++++++----
 src/backend/access/spgist/spgvacuum.c         |  25 ++-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/itup.h                     |   3 +
 src/include/access/spgist_private.h           | 129 +++++++----
 src/test/regress/expected/amutils.out         |   2 +-
 src/test/regress/expected/index_including.out |   3 +-
 .../expected/index_including_spgist.out       | 143 +++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 src/test/regress/sql/index_including.sql      |   2 +-
 19 files changed, 689 insertions(+), 193 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 671299ff05..6f321fc435 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 29dee5689e..7ff0fa3b6d 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 68d09951d9..efaf32e392 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -214,6 +214,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 634016b9b7..f5dbee3618 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -421,35 +421,54 @@ nocache_index_getattr(IndexTuple tup,
 	return fetchatt(TupleDescAttr(tupleDesc, attnum), tp + off);
 }
 
+/* Convert index tuple into Datum/isnull arrays */
+void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
+						Datum *values, bool *isnull)
+{
+	bits8      *bp;                         /* ptr to null bitmap in tuple */
+	char       *tp;                         /* ptr to tuple data */
+
+	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp,
+								 (bool) IndexTupleHasNulls(tup));
+}
+
 /*
- * Convert an index tuple into Datum/isnull arrays.
+ * Convert an index-like tuples but with arbitrary header length into
+ * Datum/isnull arrays.
  *
  * The caller must allocate sufficient storage for the output arrays.
  * (INDEX_MAX_KEYS entries should be enough.)
  *
- * This is nearly the same as heap_deform_tuple(), but for IndexTuples.
- * One difference is that the tuple should never have any missing columns.
+ * This is nearly the same as heap_deform_tuple(), but for IndexTuples and
+ * SpGistLeafTuples. One difference is that the tuple should never have any
+ * missing columns.
+ *
+ * Callers are expected to provide pointer to null bitmap, MAXALIGN-ed
+ * pointer to tuple data and hasnulls bit got from the header.
  */
 void
-index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
-				   Datum *values, bool *isnull)
+index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+							 Datum *values, bool *isnull,
+							 bits8 *bp, char *tp, bool hasnulls)
 {
-	int			hasnulls = IndexTupleHasNulls(tup);
 	int			natts = tupleDescriptor->natts; /* number of atts to extract */
 	int			attnum;
-	char	   *tp;				/* ptr to tuple data */
-	int			off;			/* offset in tuple data */
-	bits8	   *bp;				/* ptr to null bitmap in tuple */
+	int			off = 0;		/* offset in tuple data */
 	bool		slow = false;	/* can we use/set attcacheoff? */
 
 	/* Assert to protect callers who allocate fixed-size arrays */
 	Assert(natts <= INDEX_MAX_KEYS);
 
-	/* XXX "knows" t_bits are just after fixed tuple header! */
-	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	/* If has hulls, null mask should be present in a tuple */
+	Assert(hasnulls == false || ((char*) bp < tp));
 
-	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
-	off = 0;
+	/* Tuple data should start from MAXALIGN */
+	Assert(tp == (char *) MAXALIGN(tp));
 
 	for (attnum = 0; attnum < natts; attnum++)
 	{
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..ab5ff3d434 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -76,6 +76,19 @@ Leaf tuple consists of:
 
   ItemPointer to the heap
 
+  nextOffset number of next leaf tuple in a chain on a leaf page
+  optional nullmask
+  optional INCLUDE columns values
+
+Leaf tuple layout changed since PostgreSQL version 14 to support INCLUDE
+columns but in a way that doesn't change the header and the key value
+placement for a tuple without INCLUDE columns. So indexes created earlier
+remain fully supported.
+
+Nullmask is added only if there are INCLUDE columns and nulls in a tuple.
+It is added without alignment and 64-bit architectures has a good chance
+to fit alignment gap before first value which makes its storage free of
+charge.
 
 NULLS HANDLING
 
@@ -90,6 +103,10 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For INCLUDE attributes nulls are handled in ordinary per leaf-tuple way i.e.
+if null mask presence bit in a header is set, nullmask is added to tuple.
+If there is nullmask it covers all attributes, key attribute as well. It is
+redundant but allows to use index tuple code to operate SpGistLeafTuple.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..891f663471 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple, SGLT_GET_OFFSET(head));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,11 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
+	int			chainStoreIndex; /* Index for start of datums/isnulls for a
+									current chain item */
 
 	in.level = level;
 
@@ -723,14 +728,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +745,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in the chain are
+	 * collected into 2-d arrays: (number of tuples in the chain) x (number of
+	 * attributes) The first attribute is key, the other - INCLUDE attributes (if
+	 * any). After picksplit we need to form new leaf tuples as the key attribute
+	 * length can change which affects the alignment of INCLUDE attributes in a
+	 * tuple.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +774,11 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state->tupleDescriptor,
+								   &leafChainDatums[chainStoreIndex],
+								   &leafChainIsnulls[chainStoreIndex],
+								   isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +804,11 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state->tupleDescriptor,
+								   &leafChainDatums[chainStoreIndex],
+								   &leafChainIsnulls[chainStoreIndex],
+								   isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +820,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +828,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +841,19 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+	chainStoreIndex = in.nTuples * natts;
+	spgDeformLeafTuple(newLeafTuple, state->tupleDescriptor,
+					   &leafChainDatums[chainStoreIndex],
+					   &leafChainIsnulls[chainStoreIndex],
+					   isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +871,13 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) out.leafTupleDatums[i];
+			leafChainIsnulls[chainStoreIndex] = false;
+
+			newLeafs[i] = spgFormLeafTuple(state->tupleDescriptor, heapPtrs + i,
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +898,16 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) 0;
+			leafChainIsnulls[chainStoreIndex] = true;
+
+			newLeafs[i] = spgFormLeafTuple(state->tupleDescriptor, heapPtrs + i,
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1241,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1934,87 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum. Otherwise, store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[spgKeyColumn] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[spgKeyColumn]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[spgKeyColumn] = PointerGetDatum(PG_DETOAST_DATUM(datum[spgKeyColumn]));
 			else
-				leafDatum = datum;
+				leafDatum[spgKeyColumn] = datum[spgKeyColumn];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[spgKeyColumn] = (Datum) 0;
+
+	for (i = spgFirstIncludeColumn; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->tupleDescriptor, i)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+						spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+			   heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+	leafSize = leafSize < SGDTSIZE ? SGDTSIZE : MAXALIGN(leafSize);
+	leafSize += sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2026,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[spgKeyColumn] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2060,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[spgKeyColumn] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[spgKeyColumn] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2048,13 +2113,13 @@ spgdoinsert(Relation index, SpGistState *state,
 			int			nToSplit,
 						sizeToSplit;
 
-			leafTuple = spgFormLeafTuple(state, heapPtr, leafDatum, isnull);
+			leafTuple = spgFormLeafTuple(state->tupleDescriptor, heapPtr, leafDatum, isnull);
 			if (leafTuple->size + sizeof(ItemIdData) <=
 				SpGistPageGetFreeSpace(current.page, 1))
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[spgKeyColumn], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2133,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[spgKeyColumn]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[spgKeyColumn], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2175,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[spgKeyColumn];
+			in.leafDatum = leafDatum[spgKeyColumn];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2186,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[spgKeyColumn])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2223,14 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[spgKeyColumn])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[spgKeyColumn] = out.result.matchNode.restDatum;
+						leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+								spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+								heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+						/* check for leafSize < SGDTSIZE is not needed for non-null datum */
+						leafSize = MAXALIGN(leafSize) + sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2295,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..140f4a5a3e 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,28 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are INCLUDE attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.tupleDescriptor->natts > 1)
+	{
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +523,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +550,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +565,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +760,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +774,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple);
 }
 
 /*
@@ -782,8 +806,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +901,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +928,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +973,41 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.tupleDescriptor->natts > 1)
+		{
+			/* A general case of one key attribute and several INCLUDE attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.tupleDescriptor->natts));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.tupleDescriptor->natts));
+
+			spgDeformLeafTuple(leafTuple, so->state.tupleDescriptor, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * Override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[spgKeyColumn] = leafValue;
+			leafIsnulls[spgKeyColumn] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			/*
+			 * A particular case of no include attributes works exactly like more general case
+			 * above but don't make redundant allocations and rewrites when there is only one
+			 * attribute.
+			 */
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1075,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* INCLUDE attributes can always be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 64d3ba8288..8319f6e12c 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,11 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
-
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -57,7 +61,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -105,6 +109,8 @@ SpGistCache *
 spgGetCache(Relation index)
 {
 	SpGistCache *cache;
+	int i;
+	int natts = IndexRelationGetNumberOfAttributes(index);
 
 	if (index->rd_amcache == NULL)
 	{
@@ -113,18 +119,44 @@ spgGetCache(Relation index)
 		FmgrInfo   *procinfo;
 		Buffer		metabuffer;
 		SpGistMetaPageData *metadata;
+		TupleDesc 	tupleDescriptor;
+		Size		tupleDescriptorSize = offsetof(struct TupleDescData, attrs) +
+										  natts * sizeof(FormData_pg_attribute);
+
+		/*
+		 * SPGiST should have one key column and can also have INCLUDE
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
+		if (natts >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					errmsg("number of index columns (%d) exceeds limit (%d)",
+					natts, INDEX_MAX_KEYS)));
 
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
-									   sizeof(SpGistCache));
+									   offsetof(struct SpGistCache, tupleDescriptor)
+									   + tupleDescriptorSize);
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		/* Form a tuple descriptor for all columns */
+		tupleDescriptor = CreateTemplateTupleDesc(natts);
+
+		for (i = spgKeyColumn; i < natts; i++)
+		TupleDescInitEntry(tupleDescriptor, i + 1, NULL,
+				TupleDescAttr(index->rd_att, i)->atttypid, -1, 0);
+
+		memcpy(cache->tupleDescriptor, tupleDescriptor, tupleDescriptorSize);
+		pfree(tupleDescriptor);
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -196,6 +228,7 @@ initSpGistState(SpGistState *state, Relation index)
 	state->attLeafType = cache->attLeafType;
 	state->attPrefixType = cache->attPrefixType;
 	state->attLabelType = cache->attLabelType;
+	state->tupleDescriptor = cache->tupleDescriptor;
 
 	/* Make workspace for constructing dead tuples */
 	state->deadTupleStorage = palloc0(SGDTSIZE);
@@ -205,6 +238,7 @@ initSpGistState(SpGistState *state, Relation index)
 
 	/* Assume we're not in an index build (spgbuild will override) */
 	state->isBuild = false;
+
 }
 
 /*
@@ -604,8 +638,8 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
- * Also, we follow the SPGiST convention that pass-by-val types are
+ * Note the result is not maxaligned and this should be done by the caller if
+ * needed. Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
 unsigned int
@@ -620,7 +654,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,38 +676,87 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 	}
 }
 
+Size
+spgNullmaskSize(int natts, bool *isnull)
+{
+	int i;
+
+	Assert(natts > 0);
+	Assert(natts <= INDEX_MAX_KEYS);
+
+	/*
+	 * If there is only a key attribute (natts == 1), nullmask will not be inserted
+	 * (even inside the tuples, which have NULL key value). This ensures compatibility
+	 * with the previous versions tuple layout.
+	 */
+	if (natts == 1)
+		return 0;
+
+	for (i = spgKeyColumn; i < natts; i++)
+	{
+	   /*
+		* If there is at least one null, then nullmask will be sized to contain a key
+		* attribute and all INCLUDE attributes.
+		*/
+		if (isnull[i])
+			return ((natts - 1) / 8) + 1;
+	}
+	return 0;
+}
+
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Construct a leaf tuple containing the given heap TID, datums and isnulls arrays.
+ * Nullmask apply only to INCLUDE attribute and is placed just after header if
+ * there is at least one NULL among INCLUDE attributes. It doesn't need alignment.
+ * Then all attributes data follow starting from MAXALIGN.
  */
 SpGistLeafTuple
-spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+spgFormLeafTuple(TupleDesc tupleDescriptor, ItemPointer heapPtr,
+				 Datum *datum, bool *isnull)
 {
-	SpGistLeafTuple tup;
-	unsigned int size;
-
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	SpGistLeafTuple tuple;
+	uint16	tupmask = 0;
+	Size 	data_size = heap_compute_data_size(tupleDescriptor, datum, isnull);
+	Size 	nullmask_size = spgNullmaskSize(tupleDescriptor->natts, isnull);
+	Size 	hoff = MAXALIGN(sizeof(SpGistLeafTupleData) + nullmask_size);
+	Size 	size = MAXALIGN(hoff + data_size);
+	char       *tp;	/* ptr to tuple data */
 
 	/*
-	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * Ensure that we can replace the tuple with a dead tuple later. This
+	 * test is unnecessary when !isnull[spgKeyColumn], but let's be safe.
 	 */
-	if (size < SGDTSIZE)
-		size = SGDTSIZE;
+	size = size < SGDTSIZE ? SGDTSIZE : MAXALIGN(size);
 
-	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tuple = (SpGistLeafTuple) palloc0(size);
+	tuple->size = size;
+	SGLT_SET_OFFSET(tuple, InvalidOffsetNumber);
+	tuple->heapPtr = *heapPtr;
+	tp = (char *) tuple + hoff;
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
-	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
+	if (nullmask_size)
+	{
+		bits8      *bp;	/* ptr to null bitmap in tuple */
 
-	return tup;
+		/* Set nullmask presence bit in SpGistLeafTuple header if needed */
+		SGLT_SET_CONTAINSNULLMASK(tuple, true);
+		/* Fill nullmask and data part of a tuple */
+		bp = (bits8 *) ((char *)tuple + sizeof(SpGistLeafTupleData));
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask, bp);
+	}
+	else
+	{
+		/*
+		 * Prevent filling nullmask in the tuple in case there should be
+		 * no nullmask. This is important when there is only a key attribute in
+		 * the index and the particular tuple has NULL key value. But this
+		 * also fit for more general case when just nullmask_size is zero.
+		 */
+		bits8		b = '\0' ;
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask, &b);
+	}
+
+	return tuple;
 }
 
 /*
@@ -689,10 +772,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -736,7 +819,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -815,7 +898,7 @@ spgFormDeadTuple(SpGistState *state, int tupstate,
 
 	tuple->tupstate = tupstate;
 	tuple->size = SGDTSIZE;
-	tuple->nextOffset = InvalidOffsetNumber;
+	tuple->t_info = InvalidOffsetNumber;
 
 	if (tupstate == SPGIST_REDIRECT)
 	{
@@ -1047,3 +1130,47 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ */
+void
+spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+				   Datum *values, bool *isnull, bool keyColumnIsNull)
+{
+	bool		hasNullsMask = SGLT_GET_CONTAINSNULLMASK(tup);
+	char	   *tp; 						/* ptr to tuple data */
+	bits8      *bp;                         /* ptr to null bitmap in tuple */
+
+	if (keyColumnIsNull && tupleDescriptor->natts == 1)
+	{
+		/* Trivial case: there is only key attribute and we're in a nulls tree.
+		 * hasNullsMask bit in a tuple header should not be set for single
+		 * attribute case even if it has NULL value (for compatibility with
+		 * pre-v14 SpGist tuple format) We should not call
+		 * index_deform_anyheader_tuple() in this trivial case as it expects
+		 * nullmask in a tuple present in this case.
+		 */
+		Assert (hasNullsMask == 0);
+
+		isnull[spgKeyColumn] = true;
+		values[spgKeyColumn] = (Datum) 0;
+		return;
+	}
+	else if (hasNullsMask)
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData) +
+									 ((tupleDescriptor->natts - 1) / 8 + 1));
+	else
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData));
+
+	bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData));
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp, hasNullsMask);
+
+	/* Key column isnull value from a tuple should be consistent with keyColumnIsNull
+	 * got from the caller
+	 */
+	Assert (keyColumnIsNull == isnull[spgKeyColumn]);
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index e1c58933f9..c6256bcf84 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt) > max ||
+					predecessor[SGLT_GET_OFFSET(lt)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits have their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 999d0ca15d..2493f97ada 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head) == SGLT_GET_OFFSET(&leafTupleHdr));
+				SGLT_SET_OFFSET(head, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/itup.h b/src/include/access/itup.h
index b9c41d3455..5f43128720 100644
--- a/src/include/access/itup.h
+++ b/src/include/access/itup.h
@@ -154,6 +154,9 @@ extern Datum nocache_index_getattr(IndexTuple tup, int attnum,
 								   TupleDesc tupleDesc);
 extern void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
 							   Datum *values, bool *isnull);
+extern void index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+							   Datum *values, bool *isnull,
+							   bits8 *bp, char *tp, bool hasnulls);
 extern IndexTuple CopyIndexTuple(IndexTuple source);
 extern IndexTuple index_truncate_tuple(TupleDesc sourceDescriptor,
 									   IndexTuple source, int leavenatts);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..28e14aed5f 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,13 +22,15 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 } SpGistOptions;
 
+#define spgKeyColumn 0
+#define spgFirstIncludeColumn 1
+
 #define SpGistGetFillFactor(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == SPGIST_AM_OID), \
@@ -144,30 +146,11 @@ typedef struct SpGistState
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
-	TransactionId myXid;		/* XID to use when creating a redirect tuple */
-	bool		isBuild;		/* true if doing index build */
+	TransactionId  myXid;		/* XID to use when creating a redirect tuple */
+	bool		   isBuild;		/* true if doing index build */
+	TupleDesc  tupleDescriptor; /* tuple descriptor */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -243,9 +226,9 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLabelType;	/* type of node label values */
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+	TupleDescData  tupleDescriptor[FLEXIBLE_ARRAY_MEMBER]; /* descriptor for leaf tuples */
 } SpGistCache;
 
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +288,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,23 +305,21 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a heap tuple TID and columns datums and
+ * nullmasks.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of INCLUDE columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
  * wide to be stored immediately, and it costs nothing because of alignment
  * considerations.
  *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
  * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
+ * so that the tuple can be converted to REDIRECT status later. (This
  * restriction only adds bytes for the null-datum case, otherwise alignment
  * restrictions force it anyway.)
  *
@@ -346,14 +327,45 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Normally, nextOffset inside t_info links to the next tuple belonging to
+ * the same parent node (which must be on the same page).  But when the root
+ * page is a leaf page, we don't chain its tuples, so nextOffset is always 0
+ * on the root. Minimum space to store SpGistLeafTuple plus ItemIdData on a
+ * page is 16 bytes, so 15 lower bits for nextOffset is enough to store tuple
+ * number in a chain on a page even if a page size is 64Kb.
+ *
+ * The highest bit in t_info is to store per-tuple information is there nulls
+ * mask exist for the case there are INCLUDE attributes. If there are no
+ * INCLUDE columns this bit is set to 0 and nullmask is not added even if it
+ * is an empty tuple with NULL key value.
+ *
+ * Datums for all columns are stored in ordinary index-tuple-like way starting
+ * from MAXALIGN boundary. Nullmask with size (number of columns)/8
+ * bytes is put without alignment just after the ending of tuple header.
+ * On 64-bit architecture nullmask has a good chance to fit into the alignment
+ * gap between the header and the first datum, thus making its storage free
+ * of charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * t_info is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: values has nulls
+	 * 14-0 bit: nextOffset i.e. number of next tuple in chain on a page,
+	 * 			 or InvalidOffsetNumber
+	 * ---------------
+	 */
+	unsigned short t_info;	/* nextOffset for linking tuples in a chain on a leaf
+							   page, and additional info */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* nullmask follows if there are nulls among attributes*/
+	/* attributes data follow starting from MAXALIGN */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
@@ -363,6 +375,18 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
 #define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
 							 *(Datum *) SGLTDATAPTR(x) : \
 							 PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Macros to access nextOffset and bit fields inside t_info independently.
+ */
+#define SGLT_GET_OFFSET(spgLeafTuple)	( (spgLeafTuple)->t_info & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(spgLeafTuple) \
+	( (bool)((spgLeafTuple)->t_info >> 15) )
+#define SGLT_SET_OFFSET(spgLeafTuple, offsetNumber) \
+	( (spgLeafTuple)->t_info = \
+	((spgLeafTuple)->t_info & 0xC000) | ((offsetNumber) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(spgLeafTuple, is_null) \
+	( (spgLeafTuple)->t_info = \
+	((uint16)(bool)(is_null) << 15) | ((spgLeafTuple)->t_info & 0x3FFF) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -372,14 +396,14 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
  * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
  * field, to satisfy some Asserts that we make when replacing a leaf tuple
  * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
+ * We don't use t_info, but it's needed to align the pointer field.
  * pointer and xid are only valid when tupstate = REDIRECT.
  */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;
-	OffsetNumber nextOffset;	/* not used in dead tuples */
+	unsigned short t_info;		/* not used in dead tuples */
 	ItemPointerData pointer;	/* redirection inside index */
 	TransactionId xid;			/* ID of xact that inserted this tuple */
 } SpGistDeadTupleData;
@@ -394,7 +418,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +433,27 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+
 /*
  * XLOG stuff
  */
@@ -456,9 +500,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
-extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
+extern Size spgNullmaskSize(int natts, bool *isnull);
+extern SpGistLeafTuple spgFormLeafTuple(TupleDesc tupleDescriptor,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +511,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+							   Datum *datum, bool *isnull, bool keyIsNull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +531,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..7ab6113c61 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -171,7 +171,7 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  spgist | can_unique    | f
  spgist | can_multi_col | f
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..86510687c7 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -349,14 +349,13 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 
 DROP TABLE tbl;
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..213cce5c7c
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,143 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ae89ed7f0b..3b4f02a46f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 525bdc804f..c3f8faadd9 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..44b340053b 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -182,7 +182,7 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 DROP TABLE tbl;
 
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
-- 
2.28.0

#22Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#21)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

I've noticed CI error due to the fact that MSVC doesn't allow arrays of
flexible size arrays and made a fix for the issue.
Also did some minor refinement in tuple creation.
PFA v12 of a patch.

чт, 26 нояб. 2020 г. в 21:48, Pavel Borisov <pashkin.elfe@gmail.com>:

The way that seems acceptable to me is to add (optional) nulls mask into

the end of existing style SpGistLeafTuple header and use indextuple
routines to attach attributes after it. In this case, we can reduce the
amount of code at the cost of adding one extra MAXALIGN size to the

overall

tuple size on 32-bit arch as now tuple header size of 12 bit already

fits 3

MAXALIGNS (on 64 bit the header now is shorter than 2 maxaligns (12

bytes

of 16) and nulls mask will be free of cost). If you mean this I try to

make

changes soon. What do you think of it?

Yeah, that was pretty much the same conclusion I came to. For
INDEX_MAX_KEYS values up to 32, the nulls bitmap will fit into what's
now padding space on 64-bit machines. For backwards compatibility,
we'd have to be careful that the code knows there's no nulls bitmap in
an index without included columns, so I'm not sure how messy that will
be. But it's worth trying that way to see how it comes out.

I made a refactoring of the patch code according to the discussion:
1. Changed a leaf tuple format to: header - (optional) bitmask - key value
- (optional) INCLUDE values
2. Re-use existing code of heap_fill_tuple() to fill data part of a leaf
tuple
3. Splitted index_deform_tuple() into two portions: (a) bigger 'inner' one
- index_deform_anyheader_tuple() - to make processing of index-like tuples
(now IndexTuple and SpGistLeafTuple) working independent from type of tuple
header. (b) a small 'outer' index_deform_tuple() and spgDeformLeafTuple()
to make all header-specific processing and then call the inner (a)
4. Inserted a tuple descriptor into the SpGistCache chunk of memory. So
cleaning the cached chunk will also invalidate the tuple descriptor and not
make it dangling or leaked. This also allows not to build it every time
unless the cache is invalidated.
5. Corrected amroutine->amcaninclude according to new upstream fix.
6. Returned big chunks that were shifted in spgist_private.h to their
initial places where possible and made other cosmetic changes to improve
the patch.

PFA v.11 of the patch.
Do you think the proposed changes are in the right direction?

Thank you!
--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v12-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchapplication/octet-stream; name=v12-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchDownload
From 96eb57026f184f7320ac656740300c054be1eb4d Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 3 Dec 2020 16:23:51 +0400
Subject: [PATCH v12] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE columns for SPGiST index is intended to increase the speed of queries by making scans index-only likewise
in btree and GiST index. These columns are added only to leaf tuples and they are not used in index tree search but they
can be fetched during index scan.

The other point of INCLUDE columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in certain
cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster, update etc. Also, any data types without SP-GiST supported opclasses can be included.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/common/indextuple.c        |  45 ++--
 src/backend/access/spgist/README              |  17 ++
 src/backend/access/spgist/spgdoinsert.c       | 194 +++++++++++------
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  88 ++++++--
 src/backend/access/spgist/spgutils.c          | 197 ++++++++++++++----
 src/backend/access/spgist/spgvacuum.c         |  25 ++-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/itup.h                     |   3 +
 src/include/access/spgist_private.h           | 129 ++++++++----
 src/test/regress/expected/amutils.out         |   2 +-
 src/test/regress/expected/index_including.out |   3 +-
 .../expected/index_including_spgist.out       | 143 +++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 src/test/regress/sql/index_including.sql      |   2 +-
 19 files changed, 685 insertions(+), 193 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 671299ff05..6f321fc435 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 29dee5689e..7ff0fa3b6d 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 68d09951d9..efaf32e392 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -214,6 +214,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 634016b9b7..f5dbee3618 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -421,35 +421,54 @@ nocache_index_getattr(IndexTuple tup,
 	return fetchatt(TupleDescAttr(tupleDesc, attnum), tp + off);
 }
 
+/* Convert index tuple into Datum/isnull arrays */
+void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
+						Datum *values, bool *isnull)
+{
+	bits8      *bp;                         /* ptr to null bitmap in tuple */
+	char       *tp;                         /* ptr to tuple data */
+
+	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp,
+								 (bool) IndexTupleHasNulls(tup));
+}
+
 /*
- * Convert an index tuple into Datum/isnull arrays.
+ * Convert an index-like tuples but with arbitrary header length into
+ * Datum/isnull arrays.
  *
  * The caller must allocate sufficient storage for the output arrays.
  * (INDEX_MAX_KEYS entries should be enough.)
  *
- * This is nearly the same as heap_deform_tuple(), but for IndexTuples.
- * One difference is that the tuple should never have any missing columns.
+ * This is nearly the same as heap_deform_tuple(), but for IndexTuples and
+ * SpGistLeafTuples. One difference is that the tuple should never have any
+ * missing columns.
+ *
+ * Callers are expected to provide pointer to null bitmap, MAXALIGN-ed
+ * pointer to tuple data and hasnulls bit got from the header.
  */
 void
-index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
-				   Datum *values, bool *isnull)
+index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+							 Datum *values, bool *isnull,
+							 bits8 *bp, char *tp, bool hasnulls)
 {
-	int			hasnulls = IndexTupleHasNulls(tup);
 	int			natts = tupleDescriptor->natts; /* number of atts to extract */
 	int			attnum;
-	char	   *tp;				/* ptr to tuple data */
-	int			off;			/* offset in tuple data */
-	bits8	   *bp;				/* ptr to null bitmap in tuple */
+	int			off = 0;		/* offset in tuple data */
 	bool		slow = false;	/* can we use/set attcacheoff? */
 
 	/* Assert to protect callers who allocate fixed-size arrays */
 	Assert(natts <= INDEX_MAX_KEYS);
 
-	/* XXX "knows" t_bits are just after fixed tuple header! */
-	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	/* If has hulls, null mask should be present in a tuple */
+	Assert(hasnulls == false || ((char*) bp < tp));
 
-	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
-	off = 0;
+	/* Tuple data should start from MAXALIGN */
+	Assert(tp == (char *) MAXALIGN(tp));
 
 	for (attnum = 0; attnum < natts; attnum++)
 	{
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..ab5ff3d434 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -76,6 +76,19 @@ Leaf tuple consists of:
 
   ItemPointer to the heap
 
+  nextOffset number of next leaf tuple in a chain on a leaf page
+  optional nullmask
+  optional INCLUDE columns values
+
+Leaf tuple layout changed since PostgreSQL version 14 to support INCLUDE
+columns but in a way that doesn't change the header and the key value
+placement for a tuple without INCLUDE columns. So indexes created earlier
+remain fully supported.
+
+Nullmask is added only if there are INCLUDE columns and nulls in a tuple.
+It is added without alignment and 64-bit architectures has a good chance
+to fit alignment gap before first value which makes its storage free of
+charge.
 
 NULLS HANDLING
 
@@ -90,6 +103,10 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For INCLUDE attributes nulls are handled in ordinary per leaf-tuple way i.e.
+if null mask presence bit in a header is set, nullmask is added to tuple.
+If there is nullmask it covers all attributes, key attribute as well. It is
+redundant but allows to use index tuple code to operate SpGistLeafTuple.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..891f663471 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple, SGLT_GET_OFFSET(head));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,11 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
+	int			chainStoreIndex; /* Index for start of datums/isnulls for a
+									current chain item */
 
 	in.level = level;
 
@@ -723,14 +728,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +745,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in the chain are
+	 * collected into 2-d arrays: (number of tuples in the chain) x (number of
+	 * attributes) The first attribute is key, the other - INCLUDE attributes (if
+	 * any). After picksplit we need to form new leaf tuples as the key attribute
+	 * length can change which affects the alignment of INCLUDE attributes in a
+	 * tuple.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +774,11 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state->tupleDescriptor,
+								   &leafChainDatums[chainStoreIndex],
+								   &leafChainIsnulls[chainStoreIndex],
+								   isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +804,11 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state->tupleDescriptor,
+								   &leafChainDatums[chainStoreIndex],
+								   &leafChainIsnulls[chainStoreIndex],
+								   isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +820,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +828,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +841,19 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+	chainStoreIndex = in.nTuples * natts;
+	spgDeformLeafTuple(newLeafTuple, state->tupleDescriptor,
+					   &leafChainDatums[chainStoreIndex],
+					   &leafChainIsnulls[chainStoreIndex],
+					   isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +871,13 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) out.leafTupleDatums[i];
+			leafChainIsnulls[chainStoreIndex] = false;
+
+			newLeafs[i] = spgFormLeafTuple(state->tupleDescriptor, heapPtrs + i,
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +898,16 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) 0;
+			leafChainIsnulls[chainStoreIndex] = true;
+
+			newLeafs[i] = spgFormLeafTuple(state->tupleDescriptor, heapPtrs + i,
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1241,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1934,87 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum. Otherwise, store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[spgKeyColumn] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[spgKeyColumn]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[spgKeyColumn] = PointerGetDatum(PG_DETOAST_DATUM(datum[spgKeyColumn]));
 			else
-				leafDatum = datum;
+				leafDatum[spgKeyColumn] = datum[spgKeyColumn];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[spgKeyColumn] = (Datum) 0;
+
+	for (i = spgFirstIncludeColumn; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->tupleDescriptor, i)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+						spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+			   heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+	leafSize = leafSize < SGDTSIZE ? SGDTSIZE : MAXALIGN(leafSize);
+	leafSize += sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2026,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[spgKeyColumn] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2060,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[spgKeyColumn] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[spgKeyColumn] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2048,13 +2113,13 @@ spgdoinsert(Relation index, SpGistState *state,
 			int			nToSplit,
 						sizeToSplit;
 
-			leafTuple = spgFormLeafTuple(state, heapPtr, leafDatum, isnull);
+			leafTuple = spgFormLeafTuple(state->tupleDescriptor, heapPtr, leafDatum, isnull);
 			if (leafTuple->size + sizeof(ItemIdData) <=
 				SpGistPageGetFreeSpace(current.page, 1))
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[spgKeyColumn], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2133,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[spgKeyColumn]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[spgKeyColumn], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2175,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[spgKeyColumn];
+			in.leafDatum = leafDatum[spgKeyColumn];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2186,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[spgKeyColumn])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2223,14 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[spgKeyColumn])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[spgKeyColumn] = out.result.matchNode.restDatum;
+						leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+								spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+								heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+						/* check for leafSize < SGDTSIZE is not needed for non-null datum */
+						leafSize = MAXALIGN(leafSize) + sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2295,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..140f4a5a3e 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,28 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are INCLUDE attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.tupleDescriptor->natts > 1)
+	{
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +523,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +550,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +565,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +760,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +774,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple);
 }
 
 /*
@@ -782,8 +806,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +901,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +928,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +973,41 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.tupleDescriptor->natts > 1)
+		{
+			/* A general case of one key attribute and several INCLUDE attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.tupleDescriptor->natts));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.tupleDescriptor->natts));
+
+			spgDeformLeafTuple(leafTuple, so->state.tupleDescriptor, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * Override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[spgKeyColumn] = leafValue;
+			leafIsnulls[spgKeyColumn] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			/*
+			 * A particular case of no include attributes works exactly like more general case
+			 * above but don't make redundant allocations and rewrites when there is only one
+			 * attribute.
+			 */
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1075,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* INCLUDE attributes can always be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 64d3ba8288..3247963449 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,11 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
-
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -57,7 +61,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -105,6 +109,8 @@ SpGistCache *
 spgGetCache(Relation index)
 {
 	SpGistCache *cache;
+	int i;
+	int natts = IndexRelationGetNumberOfAttributes(index);
 
 	if (index->rd_amcache == NULL)
 	{
@@ -113,18 +119,42 @@ spgGetCache(Relation index)
 		FmgrInfo   *procinfo;
 		Buffer		metabuffer;
 		SpGistMetaPageData *metadata;
+		TupleDesc 	tmpTupleDescriptor;
+		/*
+		 * SPGiST should have one key column and can also have INCLUDE
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
+		if (natts >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					errmsg("number of index columns (%d) exceeds limit (%d)",
+					natts, INDEX_MAX_KEYS)));
+
+		/* Form a tuple descriptor for all columns */
+		tmpTupleDescriptor = CreateTemplateTupleDesc(natts);
+
+		for (i = spgKeyColumn; i < natts; i++)
+			TupleDescInitEntry(tmpTupleDescriptor, i + 1, NULL,
+					TupleDescAttr(index->rd_att, i)->atttypid, -1, 0);
 
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
-									   sizeof(SpGistCache));
+				offsetof(struct SpGistCache, tupleDescriptor) +
+				TupleDescSize(tmpTupleDescriptor));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		memcpy(&cache->tupleDescriptor, tmpTupleDescriptor,
+				TupleDescSize(tmpTupleDescriptor));
+		pfree(tmpTupleDescriptor);
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -196,6 +226,7 @@ initSpGistState(SpGistState *state, Relation index)
 	state->attLeafType = cache->attLeafType;
 	state->attPrefixType = cache->attPrefixType;
 	state->attLabelType = cache->attLabelType;
+	state->tupleDescriptor = &cache->tupleDescriptor;
 
 	/* Make workspace for constructing dead tuples */
 	state->deadTupleStorage = palloc0(SGDTSIZE);
@@ -205,6 +236,7 @@ initSpGistState(SpGistState *state, Relation index)
 
 	/* Assume we're not in an index build (spgbuild will override) */
 	state->isBuild = false;
+
 }
 
 /*
@@ -604,8 +636,8 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
- * Also, we follow the SPGiST convention that pass-by-val types are
+ * Note the result is not maxaligned and this should be done by the caller if
+ * needed. Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
 unsigned int
@@ -620,7 +652,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,38 +674,85 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 	}
 }
 
+Size
+spgNullmaskSize(int natts, bool *isnull)
+{
+	int i;
+
+	Assert(natts > 0);
+	Assert(natts <= INDEX_MAX_KEYS);
+
+	/*
+	 * If there is only a key attribute (natts == 1), nullmask will not be inserted
+	 * (even inside the tuples, which have NULL key value). This ensures compatibility
+	 * with the previous versions tuple layout.
+	 */
+	if (natts == 1)
+		return 0;
+
+	for (i = spgKeyColumn; i < natts; i++)
+	{
+	   /*
+		* If there is at least one null, then nullmask will be sized to contain a key
+		* attribute and all INCLUDE attributes.
+		*/
+		if (isnull[i])
+			return ((natts - 1) / 8) + 1;
+	}
+	return 0;
+}
+
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Construct a leaf tuple containing the given heap TID, datums and isnulls arrays.
+ * Nullmask apply only to INCLUDE attribute and is placed just after header if
+ * there is at least one NULL among INCLUDE attributes. It doesn't need alignment.
+ * Then all attributes data follow starting from MAXALIGN.
  */
 SpGistLeafTuple
-spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+spgFormLeafTuple(TupleDesc tupleDescriptor, ItemPointer heapPtr,
+				 Datum *datum, bool *isnull)
 {
-	SpGistLeafTuple tup;
-	unsigned int size;
-
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	SpGistLeafTuple tuple;
+	uint16	tupmask = 0;
+	Size 	data_size = heap_compute_data_size(tupleDescriptor, datum, isnull);
+	Size 	nullmask_size = spgNullmaskSize(tupleDescriptor->natts, isnull);
+	Size 	hoff = MAXALIGN(sizeof(SpGistLeafTupleData) + nullmask_size);
+	Size 	size = MAXALIGN(hoff + data_size);
+	char       *tp;	/* ptr to tuple data */
 
 	/*
-	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * Ensure that we can replace the tuple with a dead tuple later. This
+	 * test is unnecessary when !isnull[spgKeyColumn], but let's be safe.
 	 */
-	if (size < SGDTSIZE)
-		size = SGDTSIZE;
+	size = size < SGDTSIZE ? SGDTSIZE : MAXALIGN(size);
 
-	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tuple = (SpGistLeafTuple) palloc0(size);
+	tuple->size = size;
+	SGLT_SET_OFFSET(tuple, InvalidOffsetNumber);
+	tuple->heapPtr = *heapPtr;
+	tp = (char *) tuple + hoff;
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
-	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
+	if (nullmask_size)
+	{
+		bits8      *bp;	/* ptr to null bitmap in tuple */
 
-	return tup;
+		/* Set nullmask presence bit in SpGistLeafTuple header if needed */
+		SGLT_SET_CONTAINSNULLMASK(tuple, true);
+		/* Fill nullmask and data part of a tuple */
+		bp = (bits8 *) ((char *)tuple + sizeof(SpGistLeafTupleData));
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask, bp);
+	}
+	else if ( tupleDescriptor->natts > 1 || isnull[0] == false )
+		/*
+		 * Prevent filling nullmask in the tuple in case there should be
+		 * no nullmask.
+		 */
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask,
+					(bits8 *) NULL);
+
+	/* Single-column tuple with NULL value doesn't need filling data portion*/
+
+	return tuple;
 }
 
 /*
@@ -689,10 +768,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -736,7 +815,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -815,7 +894,7 @@ spgFormDeadTuple(SpGistState *state, int tupstate,
 
 	tuple->tupstate = tupstate;
 	tuple->size = SGDTSIZE;
-	tuple->nextOffset = InvalidOffsetNumber;
+	tuple->t_info = InvalidOffsetNumber;
 
 	if (tupstate == SPGIST_REDIRECT)
 	{
@@ -1047,3 +1126,47 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ */
+void
+spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+				   Datum *values, bool *isnull, bool keyColumnIsNull)
+{
+	bool		hasNullsMask = SGLT_GET_CONTAINSNULLMASK(tup);
+	char	   *tp; 						/* ptr to tuple data */
+	bits8      *bp;                         /* ptr to null bitmap in tuple */
+
+	if (keyColumnIsNull && tupleDescriptor->natts == 1)
+	{
+		/* Trivial case: there is only key attribute and we're in a nulls tree.
+		 * hasNullsMask bit in a tuple header should not be set for single
+		 * attribute case even if it has NULL value (for compatibility with
+		 * pre-v14 SpGist tuple format) We should not call
+		 * index_deform_anyheader_tuple() in this trivial case as it expects
+		 * nullmask in a tuple present in this case.
+		 */
+		Assert (hasNullsMask == 0);
+
+		isnull[spgKeyColumn] = true;
+		values[spgKeyColumn] = (Datum) 0;
+		return;
+	}
+	else if (hasNullsMask)
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData) +
+									 ((tupleDescriptor->natts - 1) / 8 + 1));
+	else
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData));
+
+	bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData));
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp, hasNullsMask);
+
+	/* Key column isnull value from a tuple should be consistent with keyColumnIsNull
+	 * got from the caller
+	 */
+	Assert (keyColumnIsNull == isnull[spgKeyColumn]);
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index e1c58933f9..c6256bcf84 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt) > max ||
+					predecessor[SGLT_GET_OFFSET(lt)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits have their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 999d0ca15d..2493f97ada 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head) == SGLT_GET_OFFSET(&leafTupleHdr));
+				SGLT_SET_OFFSET(head, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/itup.h b/src/include/access/itup.h
index b9c41d3455..5f43128720 100644
--- a/src/include/access/itup.h
+++ b/src/include/access/itup.h
@@ -154,6 +154,9 @@ extern Datum nocache_index_getattr(IndexTuple tup, int attnum,
 								   TupleDesc tupleDesc);
 extern void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
 							   Datum *values, bool *isnull);
+extern void index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+							   Datum *values, bool *isnull,
+							   bits8 *bp, char *tp, bool hasnulls);
 extern IndexTuple CopyIndexTuple(IndexTuple source);
 extern IndexTuple index_truncate_tuple(TupleDesc sourceDescriptor,
 									   IndexTuple source, int leavenatts);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..8fd4afa9df 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,13 +22,15 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 } SpGistOptions;
 
+#define spgKeyColumn 0
+#define spgFirstIncludeColumn 1
+
 #define SpGistGetFillFactor(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == SPGIST_AM_OID), \
@@ -144,30 +146,11 @@ typedef struct SpGistState
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
-	TransactionId myXid;		/* XID to use when creating a redirect tuple */
-	bool		isBuild;		/* true if doing index build */
+	TransactionId  myXid;		/* XID to use when creating a redirect tuple */
+	bool		   isBuild;		/* true if doing index build */
+	TupleDesc  tupleDescriptor; /* tuple descriptor */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -243,9 +226,9 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLabelType;	/* type of node label values */
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+	TupleDescData  tupleDescriptor;  /* descriptor for leaf tuples */
 } SpGistCache;
 
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +288,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,23 +305,21 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a heap tuple TID and columns datums and
+ * nullmasks.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of INCLUDE columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
  * wide to be stored immediately, and it costs nothing because of alignment
  * considerations.
  *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
  * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
+ * so that the tuple can be converted to REDIRECT status later. (This
  * restriction only adds bytes for the null-datum case, otherwise alignment
  * restrictions force it anyway.)
  *
@@ -346,14 +327,45 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Normally, nextOffset inside t_info links to the next tuple belonging to
+ * the same parent node (which must be on the same page).  But when the root
+ * page is a leaf page, we don't chain its tuples, so nextOffset is always 0
+ * on the root. Minimum space to store SpGistLeafTuple plus ItemIdData on a
+ * page is 16 bytes, so 15 lower bits for nextOffset is enough to store tuple
+ * number in a chain on a page even if a page size is 64Kb.
+ *
+ * The highest bit in t_info is to store per-tuple information is there nulls
+ * mask exist for the case there are INCLUDE attributes. If there are no
+ * INCLUDE columns this bit is set to 0 and nullmask is not added even if it
+ * is an empty tuple with NULL key value.
+ *
+ * Datums for all columns are stored in ordinary index-tuple-like way starting
+ * from MAXALIGN boundary. Nullmask with size (number of columns)/8
+ * bytes is put without alignment just after the ending of tuple header.
+ * On 64-bit architecture nullmask has a good chance to fit into the alignment
+ * gap between the header and the first datum, thus making its storage free
+ * of charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * t_info is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: values has nulls
+	 * 14-0 bit: nextOffset i.e. number of next tuple in chain on a page,
+	 * 			 or InvalidOffsetNumber
+	 * ---------------
+	 */
+	unsigned short t_info;	/* nextOffset for linking tuples in a chain on a leaf
+							   page, and additional info */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* nullmask follows if there are nulls among attributes*/
+	/* attributes data follow starting from MAXALIGN */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
@@ -363,6 +375,18 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
 #define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
 							 *(Datum *) SGLTDATAPTR(x) : \
 							 PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Macros to access nextOffset and bit fields inside t_info independently.
+ */
+#define SGLT_GET_OFFSET(spgLeafTuple)	( (spgLeafTuple)->t_info & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(spgLeafTuple) \
+	( (bool)((spgLeafTuple)->t_info >> 15) )
+#define SGLT_SET_OFFSET(spgLeafTuple, offsetNumber) \
+	( (spgLeafTuple)->t_info = \
+	((spgLeafTuple)->t_info & 0xC000) | ((offsetNumber) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(spgLeafTuple, is_null) \
+	( (spgLeafTuple)->t_info = \
+	((uint16)(bool)(is_null) << 15) | ((spgLeafTuple)->t_info & 0x3FFF) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -372,14 +396,14 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
  * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
  * field, to satisfy some Asserts that we make when replacing a leaf tuple
  * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
+ * We don't use t_info, but it's needed to align the pointer field.
  * pointer and xid are only valid when tupstate = REDIRECT.
  */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;
-	OffsetNumber nextOffset;	/* not used in dead tuples */
+	unsigned short t_info;		/* not used in dead tuples */
 	ItemPointerData pointer;	/* redirection inside index */
 	TransactionId xid;			/* ID of xact that inserted this tuple */
 } SpGistDeadTupleData;
@@ -394,7 +418,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +433,27 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+
 /*
  * XLOG stuff
  */
@@ -456,9 +500,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
-extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
+extern Size spgNullmaskSize(int natts, bool *isnull);
+extern SpGistLeafTuple spgFormLeafTuple(TupleDesc tupleDescriptor,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +511,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+							   Datum *datum, bool *isnull, bool keyIsNull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +531,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..7ab6113c61 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -171,7 +171,7 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  spgist | can_unique    | f
  spgist | can_multi_col | f
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..86510687c7 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -349,14 +349,13 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 
 DROP TABLE tbl;
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..213cce5c7c
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,143 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ae89ed7f0b..3b4f02a46f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 525bdc804f..c3f8faadd9 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..44b340053b 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -182,7 +182,7 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 DROP TABLE tbl;
 
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
-- 
2.28.0

#23Tom Lane
tgl@sss.pgh.pa.us
In reply to: Pavel Borisov (#22)
Re: [PATCH] Covering SPGiST index

Pavel Borisov <pashkin.elfe@gmail.com> writes:

I've noticed CI error due to the fact that MSVC doesn't allow arrays of
flexible size arrays and made a fix for the issue.
Also did some minor refinement in tuple creation.
PFA v12 of a patch.

The cfbot's still unhappy --- looks like you omitted a file from the
patch?

regards, tom lane

#24Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Tom Lane (#23)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

The cfbot's still unhappy --- looks like you omitted a file from the
patch?

You are right, thank you. Corrected this. PFA v13

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v13-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchapplication/octet-stream; name=v13-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchDownload
From ee43ddad86ed271e0a39146db6cdf07b335d4ce8 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Fri, 4 Dec 2020 21:28:27 +0400
Subject: [PATCH v13] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE columns for SPGiST index is intended to increase the speed of queries by making scans index-only likewise
in btree and GiST index. These columns are added only to leaf tuples and they are not used in index tree search but they
can be fetched during index scan.

The other point of INCLUDE columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in certain
cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster, update etc. Also, any data types without SP-GiST supported opclasses can be included.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/common/indextuple.c        |  45 ++--
 src/backend/access/spgist/README              |  17 ++
 src/backend/access/spgist/spgdoinsert.c       | 194 +++++++++++------
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  88 ++++++--
 src/backend/access/spgist/spgutils.c          | 197 ++++++++++++++----
 src/backend/access/spgist/spgvacuum.c         |  25 ++-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/itup.h                     |   3 +
 src/include/access/spgist_private.h           | 129 ++++++++----
 src/test/regress/expected/amutils.out         |   2 +-
 src/test/regress/expected/index_including.out |   3 +-
 .../expected/index_including_spgist.out       | 143 +++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 src/test/regress/sql/index_including.sql      |   2 +-
 .../regress/sql/index_including_spgist.sql    |  84 ++++++++
 20 files changed, 769 insertions(+), 193 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 671299ff05..6f321fc435 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1194,8 +1194,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 29dee5689e..7ff0fa3b6d 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index 68d09951d9..efaf32e392 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -214,6 +214,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 634016b9b7..f5dbee3618 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -421,35 +421,54 @@ nocache_index_getattr(IndexTuple tup,
 	return fetchatt(TupleDescAttr(tupleDesc, attnum), tp + off);
 }
 
+/* Convert index tuple into Datum/isnull arrays */
+void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
+						Datum *values, bool *isnull)
+{
+	bits8      *bp;                         /* ptr to null bitmap in tuple */
+	char       *tp;                         /* ptr to tuple data */
+
+	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp,
+								 (bool) IndexTupleHasNulls(tup));
+}
+
 /*
- * Convert an index tuple into Datum/isnull arrays.
+ * Convert an index-like tuples but with arbitrary header length into
+ * Datum/isnull arrays.
  *
  * The caller must allocate sufficient storage for the output arrays.
  * (INDEX_MAX_KEYS entries should be enough.)
  *
- * This is nearly the same as heap_deform_tuple(), but for IndexTuples.
- * One difference is that the tuple should never have any missing columns.
+ * This is nearly the same as heap_deform_tuple(), but for IndexTuples and
+ * SpGistLeafTuples. One difference is that the tuple should never have any
+ * missing columns.
+ *
+ * Callers are expected to provide pointer to null bitmap, MAXALIGN-ed
+ * pointer to tuple data and hasnulls bit got from the header.
  */
 void
-index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
-				   Datum *values, bool *isnull)
+index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+							 Datum *values, bool *isnull,
+							 bits8 *bp, char *tp, bool hasnulls)
 {
-	int			hasnulls = IndexTupleHasNulls(tup);
 	int			natts = tupleDescriptor->natts; /* number of atts to extract */
 	int			attnum;
-	char	   *tp;				/* ptr to tuple data */
-	int			off;			/* offset in tuple data */
-	bits8	   *bp;				/* ptr to null bitmap in tuple */
+	int			off = 0;		/* offset in tuple data */
 	bool		slow = false;	/* can we use/set attcacheoff? */
 
 	/* Assert to protect callers who allocate fixed-size arrays */
 	Assert(natts <= INDEX_MAX_KEYS);
 
-	/* XXX "knows" t_bits are just after fixed tuple header! */
-	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	/* If has hulls, null mask should be present in a tuple */
+	Assert(hasnulls == false || ((char*) bp < tp));
 
-	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
-	off = 0;
+	/* Tuple data should start from MAXALIGN */
+	Assert(tp == (char *) MAXALIGN(tp));
 
 	for (attnum = 0; attnum < natts; attnum++)
 	{
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..ab5ff3d434 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -76,6 +76,19 @@ Leaf tuple consists of:
 
   ItemPointer to the heap
 
+  nextOffset number of next leaf tuple in a chain on a leaf page
+  optional nullmask
+  optional INCLUDE columns values
+
+Leaf tuple layout changed since PostgreSQL version 14 to support INCLUDE
+columns but in a way that doesn't change the header and the key value
+placement for a tuple without INCLUDE columns. So indexes created earlier
+remain fully supported.
+
+Nullmask is added only if there are INCLUDE columns and nulls in a tuple.
+It is added without alignment and 64-bit architectures has a good chance
+to fit alignment gap before first value which makes its storage free of
+charge.
 
 NULLS HANDLING
 
@@ -90,6 +103,10 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For INCLUDE attributes nulls are handled in ordinary per leaf-tuple way i.e.
+if null mask presence bit in a header is set, nullmask is added to tuple.
+If there is nullmask it covers all attributes, key attribute as well. It is
+redundant but allows to use index tuple code to operate SpGistLeafTuple.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 934d65b89f..891f663471 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple, SGLT_GET_OFFSET(head));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -709,6 +709,11 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum	   *leafChainDatums;
+	bool	   *leafChainIsnulls;
+	const int	natts = IndexRelationGetNumberOfAttributes(index);
+	int			chainStoreIndex; /* Index for start of datums/isnulls for a
+									current chain item */
 
 	in.level = level;
 
@@ -723,14 +728,16 @@ doPickSplit(Relation index, SpGistState *state,
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
+	leafChainDatums = (Datum *) palloc(n * natts * sizeof(Datum));
+	leafChainIsnulls = (bool *) palloc(n * natts * sizeof(bool));
+
 	/*
-	 * Form list of leaf tuples which will be distributed as split result;
-	 * also, count up the amount of space that will be freed from current.
-	 * (Note that in the non-root case, we won't actually delete the old
-	 * tuples, only replace them with redirects or placeholders.)
+	 * Collect leaf tuples which will be distributed as split result; also,
+	 * count up the amount of space that will be freed from current. (Note
+	 * that in the non-root case, we won't actually delete the old tuples,
+	 * only replace them with redirects or placeholders.)
 	 *
 	 * Note: the SGLTDATUM calls here are safe even when dealing with a nulls
 	 * page.  For a pass-by-value data type we will fetch a word that must
@@ -738,7 +745,15 @@ doPickSplit(Relation index, SpGistState *state,
 	 * tuples must have size at least SGDTSIZE).  For a pass-by-reference type
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
+	 *
+	 * Datums and isnulls of all leaf tuple attributes in the chain are
+	 * collected into 2-d arrays: (number of tuples in the chain) x (number of
+	 * attributes) The first attribute is key, the other - INCLUDE attributes (if
+	 * any). After picksplit we need to form new leaf tuples as the key attribute
+	 * length can change which affects the alignment of INCLUDE attributes in a
+	 * tuple.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -759,6 +774,11 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state->tupleDescriptor,
+								   &leafChainDatums[chainStoreIndex],
+								   &leafChainIsnulls[chainStoreIndex],
+								   isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -784,6 +804,11 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
 				heapPtrs[nToInsert] = it->heapPtr;
+				chainStoreIndex = nToInsert * natts;
+				spgDeformLeafTuple(it, state->tupleDescriptor,
+								   &leafChainDatums[chainStoreIndex],
+								   &leafChainIsnulls[chainStoreIndex],
+								   isNulls);
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +820,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +828,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -816,10 +841,19 @@ doPickSplit(Relation index, SpGistState *state,
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
 	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+	chainStoreIndex = in.nTuples * natts;
+	spgDeformLeafTuple(newLeafTuple, state->tupleDescriptor,
+					   &leafChainDatums[chainStoreIndex],
+					   &leafChainIsnulls[chainStoreIndex],
+					   isNulls);
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
 
+	/*
+	 * Process collected key values of tuples from the chain. Included values
+	 * are used to build fresh leaf tuples unchanged.
+	 */
 	if (!isNulls)
 	{
 		/*
@@ -837,9 +871,13 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) out.leafTupleDatums[i];
+			leafChainIsnulls[chainStoreIndex] = false;
+
+			newLeafs[i] = spgFormLeafTuple(state->tupleDescriptor, heapPtrs + i,
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +898,16 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			chainStoreIndex = i * natts;
+			leafChainDatums[chainStoreIndex] = (Datum) 0;
+			leafChainIsnulls[chainStoreIndex] = true;
+
+			newLeafs[i] = spgFormLeafTuple(state->tupleDescriptor, heapPtrs + i,
+										   &leafChainDatums[chainStoreIndex],
+										   &leafChainIsnulls[chainStoreIndex]);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1241,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1934,87 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * key datum from the input datum. Otherwise, store the input datum as is.
+	 * Since we don't use index_form_tuple in this AM, we have to make sure
+	 * value to be inserted is not toasted; FormIndexDatum doesn't guarantee
+	 * that.  But we assume the "compress" method to return an untoasted
+	 * value.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[spgKeyColumn] = FunctionCall1Coll(compressProcinfo,
+											 index->rd_indcollation[0],
+											 datum[spgKeyColumn]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[spgKeyColumn] = PointerGetDatum(PG_DETOAST_DATUM(datum[spgKeyColumn]));
 			else
-				leafDatum = datum;
+				leafDatum[spgKeyColumn] = datum[spgKeyColumn];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[spgKeyColumn] = (Datum) 0;
+
+	for (i = spgFirstIncludeColumn; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->tupleDescriptor, i)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+						spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+			   heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+	leafSize = leafSize < SGDTSIZE ? SGDTSIZE : MAXALIGN(leafSize);
+	leafSize += sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2026,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[spgKeyColumn] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2060,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[spgKeyColumn] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2102,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[spgKeyColumn] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2048,13 +2113,13 @@ spgdoinsert(Relation index, SpGistState *state,
 			int			nToSplit,
 						sizeToSplit;
 
-			leafTuple = spgFormLeafTuple(state, heapPtr, leafDatum, isnull);
+			leafTuple = spgFormLeafTuple(state->tupleDescriptor, heapPtr, leafDatum, isnull);
 			if (leafTuple->size + sizeof(ItemIdData) <=
 				SpGistPageGetFreeSpace(current.page, 1))
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[spgKeyColumn], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2133,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[spgKeyColumn]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[spgKeyColumn], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2175,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[spgKeyColumn];
+			in.leafDatum = leafDatum[spgKeyColumn];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2186,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[spgKeyColumn])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2223,14 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[spgKeyColumn])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[spgKeyColumn] = out.result.matchNode.restDatum;
+						leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+								spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+								heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+						/* check for leafSize < SGDTSIZE is not needed for non-null datum */
+						leafSize = MAXALIGN(leafSize) + sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2295,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index e4508a2b92..b54ae85f6e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -226,7 +225,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 4d506bfb9a..140f4a5a3e 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,28 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are INCLUDE attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.tupleDescriptor->natts > 1)
+	{
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +523,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +550,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +565,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +760,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +774,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple);
 }
 
 /*
@@ -782,8 +806,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +901,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +928,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +973,41 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.tupleDescriptor->natts > 1)
+		{
+			/* A general case of one key attribute and several INCLUDE attributes */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.tupleDescriptor->natts));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.tupleDescriptor->natts));
+
+			spgDeformLeafTuple(leafTuple, so->state.tupleDescriptor, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * Override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[spgKeyColumn] = leafValue;
+			leafIsnulls[spgKeyColumn] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			/*
+			 * A particular case of no include attributes works exactly like more general case
+			 * above but don't make redundant allocations and rewrites when there is only one
+			 * attribute.
+			 */
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1075,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* INCLUDE attributes can always be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index 64d3ba8288..3247963449 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,11 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
-
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -57,7 +61,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -105,6 +109,8 @@ SpGistCache *
 spgGetCache(Relation index)
 {
 	SpGistCache *cache;
+	int i;
+	int natts = IndexRelationGetNumberOfAttributes(index);
 
 	if (index->rd_amcache == NULL)
 	{
@@ -113,18 +119,42 @@ spgGetCache(Relation index)
 		FmgrInfo   *procinfo;
 		Buffer		metabuffer;
 		SpGistMetaPageData *metadata;
+		TupleDesc 	tmpTupleDescriptor;
+		/*
+		 * SPGiST should have one key column and can also have INCLUDE
+		 * columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
+		if (natts >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					errmsg("number of index columns (%d) exceeds limit (%d)",
+					natts, INDEX_MAX_KEYS)));
+
+		/* Form a tuple descriptor for all columns */
+		tmpTupleDescriptor = CreateTemplateTupleDesc(natts);
+
+		for (i = spgKeyColumn; i < natts; i++)
+			TupleDescInitEntry(tmpTupleDescriptor, i + 1, NULL,
+					TupleDescAttr(index->rd_att, i)->atttypid, -1, 0);
 
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
-									   sizeof(SpGistCache));
+				offsetof(struct SpGistCache, tupleDescriptor) +
+				TupleDescSize(tmpTupleDescriptor));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		memcpy(&cache->tupleDescriptor, tmpTupleDescriptor,
+				TupleDescSize(tmpTupleDescriptor));
+		pfree(tmpTupleDescriptor);
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -196,6 +226,7 @@ initSpGistState(SpGistState *state, Relation index)
 	state->attLeafType = cache->attLeafType;
 	state->attPrefixType = cache->attPrefixType;
 	state->attLabelType = cache->attLabelType;
+	state->tupleDescriptor = &cache->tupleDescriptor;
 
 	/* Make workspace for constructing dead tuples */
 	state->deadTupleStorage = palloc0(SGDTSIZE);
@@ -205,6 +236,7 @@ initSpGistState(SpGistState *state, Relation index)
 
 	/* Assume we're not in an index build (spgbuild will override) */
 	state->isBuild = false;
+
 }
 
 /*
@@ -604,8 +636,8 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
- * Also, we follow the SPGiST convention that pass-by-val types are
+ * Note the result is not maxaligned and this should be done by the caller if
+ * needed. Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
 unsigned int
@@ -620,7 +652,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,38 +674,85 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 	}
 }
 
+Size
+spgNullmaskSize(int natts, bool *isnull)
+{
+	int i;
+
+	Assert(natts > 0);
+	Assert(natts <= INDEX_MAX_KEYS);
+
+	/*
+	 * If there is only a key attribute (natts == 1), nullmask will not be inserted
+	 * (even inside the tuples, which have NULL key value). This ensures compatibility
+	 * with the previous versions tuple layout.
+	 */
+	if (natts == 1)
+		return 0;
+
+	for (i = spgKeyColumn; i < natts; i++)
+	{
+	   /*
+		* If there is at least one null, then nullmask will be sized to contain a key
+		* attribute and all INCLUDE attributes.
+		*/
+		if (isnull[i])
+			return ((natts - 1) / 8) + 1;
+	}
+	return 0;
+}
+
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Construct a leaf tuple containing the given heap TID, datums and isnulls arrays.
+ * Nullmask apply only to INCLUDE attribute and is placed just after header if
+ * there is at least one NULL among INCLUDE attributes. It doesn't need alignment.
+ * Then all attributes data follow starting from MAXALIGN.
  */
 SpGistLeafTuple
-spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+spgFormLeafTuple(TupleDesc tupleDescriptor, ItemPointer heapPtr,
+				 Datum *datum, bool *isnull)
 {
-	SpGistLeafTuple tup;
-	unsigned int size;
-
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	SpGistLeafTuple tuple;
+	uint16	tupmask = 0;
+	Size 	data_size = heap_compute_data_size(tupleDescriptor, datum, isnull);
+	Size 	nullmask_size = spgNullmaskSize(tupleDescriptor->natts, isnull);
+	Size 	hoff = MAXALIGN(sizeof(SpGistLeafTupleData) + nullmask_size);
+	Size 	size = MAXALIGN(hoff + data_size);
+	char       *tp;	/* ptr to tuple data */
 
 	/*
-	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * Ensure that we can replace the tuple with a dead tuple later. This
+	 * test is unnecessary when !isnull[spgKeyColumn], but let's be safe.
 	 */
-	if (size < SGDTSIZE)
-		size = SGDTSIZE;
+	size = size < SGDTSIZE ? SGDTSIZE : MAXALIGN(size);
 
-	/* OK, form the tuple */
-	tup = (SpGistLeafTuple) palloc0(size);
+	tuple = (SpGistLeafTuple) palloc0(size);
+	tuple->size = size;
+	SGLT_SET_OFFSET(tuple, InvalidOffsetNumber);
+	tuple->heapPtr = *heapPtr;
+	tp = (char *) tuple + hoff;
 
-	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
-	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
+	if (nullmask_size)
+	{
+		bits8      *bp;	/* ptr to null bitmap in tuple */
 
-	return tup;
+		/* Set nullmask presence bit in SpGistLeafTuple header if needed */
+		SGLT_SET_CONTAINSNULLMASK(tuple, true);
+		/* Fill nullmask and data part of a tuple */
+		bp = (bits8 *) ((char *)tuple + sizeof(SpGistLeafTupleData));
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask, bp);
+	}
+	else if ( tupleDescriptor->natts > 1 || isnull[0] == false )
+		/*
+		 * Prevent filling nullmask in the tuple in case there should be
+		 * no nullmask.
+		 */
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask,
+					(bits8 *) NULL);
+
+	/* Single-column tuple with NULL value doesn't need filling data portion*/
+
+	return tuple;
 }
 
 /*
@@ -689,10 +768,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -736,7 +815,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -815,7 +894,7 @@ spgFormDeadTuple(SpGistState *state, int tupstate,
 
 	tuple->tupstate = tupstate;
 	tuple->size = SGDTSIZE;
-	tuple->nextOffset = InvalidOffsetNumber;
+	tuple->t_info = InvalidOffsetNumber;
 
 	if (tupstate == SPGIST_REDIRECT)
 	{
@@ -1047,3 +1126,47 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ */
+void
+spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+				   Datum *values, bool *isnull, bool keyColumnIsNull)
+{
+	bool		hasNullsMask = SGLT_GET_CONTAINSNULLMASK(tup);
+	char	   *tp; 						/* ptr to tuple data */
+	bits8      *bp;                         /* ptr to null bitmap in tuple */
+
+	if (keyColumnIsNull && tupleDescriptor->natts == 1)
+	{
+		/* Trivial case: there is only key attribute and we're in a nulls tree.
+		 * hasNullsMask bit in a tuple header should not be set for single
+		 * attribute case even if it has NULL value (for compatibility with
+		 * pre-v14 SpGist tuple format) We should not call
+		 * index_deform_anyheader_tuple() in this trivial case as it expects
+		 * nullmask in a tuple present in this case.
+		 */
+		Assert (hasNullsMask == 0);
+
+		isnull[spgKeyColumn] = true;
+		values[spgKeyColumn] = (Datum) 0;
+		return;
+	}
+	else if (hasNullsMask)
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData) +
+									 ((tupleDescriptor->natts - 1) / 8 + 1));
+	else
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData));
+
+	bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData));
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp, hasNullsMask);
+
+	/* Key column isnull value from a tuple should be consistent with keyColumnIsNull
+	 * got from the caller
+	 */
+	Assert (keyColumnIsNull == isnull[spgKeyColumn]);
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index e1c58933f9..c6256bcf84 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt) > max ||
+					predecessor[SGLT_GET_OFFSET(lt)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits have their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index 999d0ca15d..2493f97ada 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head) == SGLT_GET_OFFSET(&leafTupleHdr));
+				SGLT_SET_OFFSET(head, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/itup.h b/src/include/access/itup.h
index b9c41d3455..5f43128720 100644
--- a/src/include/access/itup.h
+++ b/src/include/access/itup.h
@@ -154,6 +154,9 @@ extern Datum nocache_index_getattr(IndexTuple tup, int attnum,
 								   TupleDesc tupleDesc);
 extern void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
 							   Datum *values, bool *isnull);
+extern void index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+							   Datum *values, bool *isnull,
+							   bits8 *bp, char *tp, bool hasnulls);
 extern IndexTuple CopyIndexTuple(IndexTuple source);
 extern IndexTuple index_truncate_tuple(TupleDesc sourceDescriptor,
 									   IndexTuple source, int leavenatts);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 00b98ec6a0..8fd4afa9df 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,13 +22,15 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 } SpGistOptions;
 
+#define spgKeyColumn 0
+#define spgFirstIncludeColumn 1
+
 #define SpGistGetFillFactor(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == SPGIST_AM_OID), \
@@ -144,30 +146,11 @@ typedef struct SpGistState
 
 	char	   *deadTupleStorage;	/* workspace for spgFormDeadTuple */
 
-	TransactionId myXid;		/* XID to use when creating a redirect tuple */
-	bool		isBuild;		/* true if doing index build */
+	TransactionId  myXid;		/* XID to use when creating a redirect tuple */
+	bool		   isBuild;		/* true if doing index build */
+	TupleDesc  tupleDescriptor; /* tuple descriptor */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -243,9 +226,9 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLabelType;	/* type of node label values */
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+	TupleDescData  tupleDescriptor;  /* descriptor for leaf tuples */
 } SpGistCache;
 
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +288,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,23 +305,21 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a heap tuple TID and columns datums and
+ * nullmasks.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of INCLUDE columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
  * wide to be stored immediately, and it costs nothing because of alignment
  * considerations.
  *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
  * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
+ * so that the tuple can be converted to REDIRECT status later. (This
  * restriction only adds bytes for the null-datum case, otherwise alignment
  * restrictions force it anyway.)
  *
@@ -346,14 +327,45 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Normally, nextOffset inside t_info links to the next tuple belonging to
+ * the same parent node (which must be on the same page).  But when the root
+ * page is a leaf page, we don't chain its tuples, so nextOffset is always 0
+ * on the root. Minimum space to store SpGistLeafTuple plus ItemIdData on a
+ * page is 16 bytes, so 15 lower bits for nextOffset is enough to store tuple
+ * number in a chain on a page even if a page size is 64Kb.
+ *
+ * The highest bit in t_info is to store per-tuple information is there nulls
+ * mask exist for the case there are INCLUDE attributes. If there are no
+ * INCLUDE columns this bit is set to 0 and nullmask is not added even if it
+ * is an empty tuple with NULL key value.
+ *
+ * Datums for all columns are stored in ordinary index-tuple-like way starting
+ * from MAXALIGN boundary. Nullmask with size (number of columns)/8
+ * bytes is put without alignment just after the ending of tuple header.
+ * On 64-bit architecture nullmask has a good chance to fit into the alignment
+ * gap between the header and the first datum, thus making its storage free
+ * of charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * t_info is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: values has nulls
+	 * 14-0 bit: nextOffset i.e. number of next tuple in chain on a page,
+	 * 			 or InvalidOffsetNumber
+	 * ---------------
+	 */
+	unsigned short t_info;	/* nextOffset for linking tuples in a chain on a leaf
+							   page, and additional info */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* nullmask follows if there are nulls among attributes*/
+	/* attributes data follow starting from MAXALIGN */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
@@ -363,6 +375,18 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
 #define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
 							 *(Datum *) SGLTDATAPTR(x) : \
 							 PointerGetDatum(SGLTDATAPTR(x)))
+/*
+ * Macros to access nextOffset and bit fields inside t_info independently.
+ */
+#define SGLT_GET_OFFSET(spgLeafTuple)	( (spgLeafTuple)->t_info & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(spgLeafTuple) \
+	( (bool)((spgLeafTuple)->t_info >> 15) )
+#define SGLT_SET_OFFSET(spgLeafTuple, offsetNumber) \
+	( (spgLeafTuple)->t_info = \
+	((spgLeafTuple)->t_info & 0xC000) | ((offsetNumber) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(spgLeafTuple, is_null) \
+	( (spgLeafTuple)->t_info = \
+	((uint16)(bool)(is_null) << 15) | ((spgLeafTuple)->t_info & 0x3FFF) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -372,14 +396,14 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
  * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
  * field, to satisfy some Asserts that we make when replacing a leaf tuple
  * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
+ * We don't use t_info, but it's needed to align the pointer field.
  * pointer and xid are only valid when tupstate = REDIRECT.
  */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;
-	OffsetNumber nextOffset;	/* not used in dead tuples */
+	unsigned short t_info;		/* not used in dead tuples */
 	ItemPointerData pointer;	/* redirection inside index */
 	TransactionId xid;			/* ID of xact that inserted this tuple */
 } SpGistDeadTupleData;
@@ -394,7 +418,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +433,27 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+
 /*
  * XLOG stuff
  */
@@ -456,9 +500,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
-extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
+extern Size spgNullmaskSize(int natts, bool *isnull);
+extern SpGistLeafTuple spgFormLeafTuple(TupleDesc tupleDescriptor,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +511,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+							   Datum *datum, bool *isnull, bool keyIsNull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +531,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..7ab6113c61 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -171,7 +171,7 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  spgist | can_unique    | f
  spgist | can_multi_col | f
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..86510687c7 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -349,14 +349,13 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 
 DROP TABLE tbl;
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..213cce5c7c
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,143 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ae89ed7f0b..3b4f02a46f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -50,7 +50,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 525bdc804f..c3f8faadd9 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..44b340053b 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -182,7 +182,7 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 DROP TABLE tbl;
 
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..38ace74d4e
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,84 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
-- 
2.28.0

#25David Steele
david@pgmasters.net
In reply to: Pavel Borisov (#24)
Re: [PATCH] Covering SPGiST index

On 12/4/20 12:31 PM, Pavel Borisov wrote:

The cfbot's still unhappy --- looks like you omitted a file from the
patch?

You are right, thank you. Corrected this. PFA v13

Tom, do the changes as enumerated in [1]/messages/by-id/CALT9ZEEszJUwsXMWknXQ3k_YbGtQaQwiYxxEGZ-pFGRUDSXdtQ@mail.gmail.com look like they are going in the
right direction?

Regards,
--
-David
david@pgmasters.net

[1]: /messages/by-id/CALT9ZEEszJUwsXMWknXQ3k_YbGtQaQwiYxxEGZ-pFGRUDSXdtQ@mail.gmail.com
/messages/by-id/CALT9ZEEszJUwsXMWknXQ3k_YbGtQaQwiYxxEGZ-pFGRUDSXdtQ@mail.gmail.com

#26Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Steele (#25)
Re: [PATCH] Covering SPGiST index

David Steele <david@pgmasters.net> writes:

Tom, do the changes as enumerated in [1] look like they are going in the
right direction?

I spent a little time looking at this, and realized something that may
or may not be a serious problem. This form of the patch supposes that
it can use the usual tuple form/deform logic for all columns of a leaf
tuple including the key column. However, that does not square with
SPGIST's existing storage convention for pass-by-value key types: we
presently assume that those are stored in their Datum representation,
ie always 4 or 8 bytes depending on machine word width, even when
typlen is less than that.

Now there is an argument why this might not be an unacceptable disk
format breakage: there probably aren't any SPGIST indexes with a
pass-by-value leaf key type. We certainly haven't got any such
opclasses in core, and it's a bit hard to see what the semantics or
use-case would be for indexing bools or smallints with SPGIST.
However, doing nothing isn't okay, because if anyone did make such
an opclass in future, it'd misbehave with this patch (since SGLTDATUM
would disagree with the actual storage layout).

There are a number of options we could consider:

1. Disallow pass-by-value leafType, checking this in spgGetCache().
The main advantage of this IMV is that if anyone has written such an
opclass already, it'd break noisily rather than silently misbehave.
It'd also allow simplification of SGLTDATUM by removing its
pass-by-value case, which is kind of nice.

2. Accept the potential format breakage, and keep the patch mostly
as-is but adjust SGLTDATUM to do the right thing depending on typlen.

3. Decide that we need to preserve the existing rule. We could hackily
still use the standard tuple form/deform logic if we told it that the
datatype of a pass-by-value key column is INT4 or INT8, depending on
sizeof(Datum). But that could be rather messy.

Another thing I notice in this immediate area is that the code
presently assumes it can apply SGLTDATUM even to leaf tuples that
store a null key. That's perfectly okay for pass-by-ref key types,
since it just means we compute an address we're not going to
dereference. But it's really rather broken for pass-by-value cases:
it'll fetch a word from past the end of the tuple. Given recent
musings about making the holes in the middle of pages undefined per
valgrind, I wonder whether we aren't going to be forced to clean that
up. Choice #1 looks a little more attractive with that in mind: it'd
mean there's nothing to fix.

A couple of other observations:

* Making doPickSplit deform all the tuples at once, and thereby need
fairly large work arrays (which it leaks), seems kind of ugly.
Couldn't we leave the deforming to the end, and do it one tuple at
a time just as we form the derived tuples? (Then you could use
fixed-size local arrays of length INDEX_MAX_KEYS.) Could probably
remove the heapPtrs[] array that way, too.

* Personally I would not have changed the API of spgFormLeafTuple
to take only the TupleDesc and not the whole SpGistState. That
doesn't seem to buy anything, and we'd have to undo it in future
if spgFormLeafTuple ever needs access to any of the rest of the
SpGistState.

* The amount of random whitespace changes in the patch is really
rather annoying. Please run the code through pgindent to undo
unnecessary changes to existing code lines, and also look through
it to remove unnecessary additions and removals of blank lines.

regards, tom lane

#27Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#26)
Re: [PATCH] Covering SPGiST index

I wrote:

I spent a little time looking at this, and realized something that may
or may not be a serious problem. This form of the patch supposes that
it can use the usual tuple form/deform logic for all columns of a leaf
tuple including the key column. However, that does not square with
SPGIST's existing storage convention for pass-by-value key types: we
presently assume that those are stored in their Datum representation,
ie always 4 or 8 bytes depending on machine word width, even when
typlen is less than that.

Now there is an argument why this might not be an unacceptable disk
format breakage: there probably aren't any SPGIST indexes with a
pass-by-value leaf key type.

On further contemplation, it occurs to me that if we make the switch
to "key values are stored per normal rules", then even if there is an
index with pass-by-value keys out there someplace, it would only break
on big-endian architectures. On little-endian, the extra space
occupied by the Datum format would just seem to be padding space.
So this probably means that the theoretical compatibility hazard is
small enough to be negligible, and we should go with my option #2
(i.e., we need to replace SGLTDATUM with normal attribute-fetch logic).

regards, tom lane

#28Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Tom Lane (#27)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

On further contemplation, it occurs to me that if we make the switch
to "key values are stored per normal rules", then even if there is an
index with pass-by-value keys out there someplace, it would only break
on big-endian architectures. On little-endian, the extra space
occupied by the Datum format would just seem to be padding space.
So this probably means that the theoretical compatibility hazard is
small enough to be negligible, and we should go with my option #2
(i.e., we need to replace SGLTDATUM with normal attribute-fetch logic).

regards, tom lane

I am sorry for the delay in reply. Now I've returned to the work on the
patch.
First of all big thanks for good pieces of advice. I especially liked the
idea of not allocating a big array in a picksplit procedure and doing
deform and form tuples on the fly.
I found all notes mentioned are quite worth integrating into the patch, and
have made the next version of a patch (with a pgindent done also). PFA v 14.

I hope I understand the way to modify SGLTDATUM in the right way. If not
please let me know. (The macro SGLTDATUM itself is not removed, it calls
fetch_att. And I find this suitable as the address for the first tuple
attribute is MAXALIGNed).

Thanks again for your consideration. From now I hope to be able to work on
the feature with not so big delay.

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v14-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchapplication/octet-stream; name=v14-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchDownload
From f0b18f225e78acfc2ffed3af008ad2a5f645eb15 Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 25 Mar 2021 23:32:58 +0400
Subject: [PATCH v14] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE columns for SPGiST index is intended to increase the speed of queries by making scans index-only likewise
in btree and GiST index. These columns are added only to leaf tuples and they are not used in index tree search but they
can be fetched during index scan.

The other point of INCLUDE columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in certain
cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster, update etc. Also, any data types without SP-GiST supported opclasses can be included.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/common/indextuple.c        |  46 +++--
 src/backend/access/spgist/README              |  17 ++
 src/backend/access/spgist/spgdoinsert.c       | 174 ++++++++++------
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  91 +++++++--
 src/backend/access/spgist/spgutils.c          | 191 +++++++++++++++---
 src/backend/access/spgist/spgvacuum.c         |  25 ++-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/itup.h                     |   3 +
 src/include/access/spgist_private.h           | 128 ++++++++----
 src/test/regress/expected/amutils.out         |   2 +-
 src/test/regress/expected/index_including.out |   3 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 src/test/regress/sql/index_including.sql      |   2 +-
 18 files changed, 524 insertions(+), 188 deletions(-)

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 623962d1d8..7a6bb03bef 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1208,8 +1208,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 682af4bbe4..cb74c998b1 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index ea88ae45e5..148b5629b7 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -214,6 +214,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1f6b7b77d4..f4819fbed0 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -422,35 +422,55 @@ nocache_index_getattr(IndexTuple tup,
 	return fetchatt(TupleDescAttr(tupleDesc, attnum), tp + off);
 }
 
+/* Convert index tuple into Datum/isnull arrays */
+void
+index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
+				   Datum *values, bool *isnull)
+{
+	bits8	   *bp;				/* ptr to null bitmap in tuple */
+	char	   *tp;				/* ptr to tuple data */
+
+	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp,
+								 (bool) IndexTupleHasNulls(tup));
+}
+
 /*
- * Convert an index tuple into Datum/isnull arrays.
+ * Convert an index-like tuples but with arbitrary header length into
+ * Datum/isnull arrays.
  *
  * The caller must allocate sufficient storage for the output arrays.
  * (INDEX_MAX_KEYS entries should be enough.)
  *
- * This is nearly the same as heap_deform_tuple(), but for IndexTuples.
- * One difference is that the tuple should never have any missing columns.
+ * This is nearly the same as heap_deform_tuple(), but for IndexTuples and
+ * SpGistLeafTuples. One difference is that the tuple should never have any
+ * missing columns.
+ *
+ * Callers are expected to provide pointer to null bitmap, MAXALIGN-ed
+ * pointer to tuple data and hasnulls bit got from the header.
  */
 void
-index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
-				   Datum *values, bool *isnull)
+index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+							 Datum *values, bool *isnull,
+							 bits8 *bp, char *tp, bool hasnulls)
 {
-	int			hasnulls = IndexTupleHasNulls(tup);
 	int			natts = tupleDescriptor->natts; /* number of atts to extract */
 	int			attnum;
-	char	   *tp;				/* ptr to tuple data */
-	int			off;			/* offset in tuple data */
-	bits8	   *bp;				/* ptr to null bitmap in tuple */
+	int			off = 0;		/* offset in tuple data */
 	bool		slow = false;	/* can we use/set attcacheoff? */
 
 	/* Assert to protect callers who allocate fixed-size arrays */
 	Assert(natts <= INDEX_MAX_KEYS);
 
-	/* XXX "knows" t_bits are just after fixed tuple header! */
-	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	/* If has hulls, null mask should be present in a tuple */
+	Assert(hasnulls == false || ((char *) bp < tp));
 
-	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
-	off = 0;
+	/* Tuple data should start from MAXALIGN */
+	Assert(tp == (char *) MAXALIGN(tp));
 
 	for (attnum = 0; attnum < natts; attnum++)
 	{
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..ab5ff3d434 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -76,6 +76,19 @@ Leaf tuple consists of:
 
   ItemPointer to the heap
 
+  nextOffset number of next leaf tuple in a chain on a leaf page
+  optional nullmask
+  optional INCLUDE columns values
+
+Leaf tuple layout changed since PostgreSQL version 14 to support INCLUDE
+columns but in a way that doesn't change the header and the key value
+placement for a tuple without INCLUDE columns. So indexes created earlier
+remain fully supported.
+
+Nullmask is added only if there are INCLUDE columns and nulls in a tuple.
+It is added without alignment and 64-bit architectures has a good chance
+to fit alignment gap before first value which makes its storage free of
+charge.
 
 NULLS HANDLING
 
@@ -90,6 +103,10 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For INCLUDE attributes nulls are handled in ordinary per leaf-tuple way i.e.
+if null mask presence bit in a header is set, nullmask is added to tuple.
+If there is nullmask it covers all attributes, key attribute as well. It is
+redundant but allows to use index tuple code to operate SpGistLeafTuple.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 7bd269fd2a..01a0ee0921 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple, SGLT_GET_OFFSET(head));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -690,13 +690,13 @@ doPickSplit(Relation index, SpGistState *state,
 			   *nodes;
 	Buffer		newInnerBuffer,
 				newLeafBuffer;
-	ItemPointerData *heapPtrs;
 	uint8	   *leafPageSelect;
 	int		   *leafSizes;
 	OffsetNumber *toDelete;
 	OffsetNumber *toInsert;
 	OffsetNumber redirectTuplePos = InvalidOffsetNumber;
 	OffsetNumber startOffsets[2];
+	SpGistLeafTuple *oldLeafs;
 	SpGistLeafTuple *newLeafs;
 	int			spaceToDelete;
 	int			currentFreeSpace;
@@ -709,6 +709,8 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum		leafDatums[INDEX_MAX_KEYS];
+	bool		leafIsnulls[INDEX_MAX_KEYS];
 
 	in.level = level;
 
@@ -718,12 +720,11 @@ doPickSplit(Relation index, SpGistState *state,
 	max = PageGetMaxOffsetNumber(current->page);
 	n = max + 1;
 	in.datums = (Datum *) palloc(sizeof(Datum) * n);
-	heapPtrs = (ItemPointerData *) palloc(sizeof(ItemPointerData) * n);
 	toDelete = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
+	oldLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
 	/*
@@ -739,6 +740,7 @@ doPickSplit(Relation index, SpGistState *state,
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -758,7 +760,7 @@ doPickSplit(Relation index, SpGistState *state,
 			if (it->tupstate == SPGIST_LIVE)
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
-				heapPtrs[nToInsert] = it->heapPtr;
+				oldLeafs[nToInsert] = it;
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -783,7 +785,7 @@ doPickSplit(Relation index, SpGistState *state,
 			if (it->tupstate == SPGIST_LIVE)
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
-				heapPtrs[nToInsert] = it->heapPtr;
+				oldLeafs[nToInsert] = it;
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +797,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +805,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -815,7 +817,7 @@ doPickSplit(Relation index, SpGistState *state,
 	 * for the picksplit function.  So don't increment nToInsert yet.
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
-	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+	oldLeafs[in.nTuples] = newLeafTuple;
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
@@ -837,9 +839,17 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			spgDeformLeafTuple(oldLeafs[i], state->tupleDescriptor,
+							   leafDatums,
+							   leafIsnulls,
+							   isNulls);
+
+			leafDatums[spgKeyColumn] = (Datum) out.leafTupleDatums[i];
+			leafIsnulls[spgKeyColumn] = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, &oldLeafs[i]->heapPtr,
+										   leafDatums,
+										   leafIsnulls);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +870,20 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			spgDeformLeafTuple(oldLeafs[i], state->tupleDescriptor,
+							   leafDatums,
+							   leafIsnulls,
+							   isNulls);
+
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			leafDatums[spgKeyColumn] = (Datum) 0;
+			leafIsnulls[spgKeyColumn] = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, &oldLeafs[i]->heapPtr,
+										   leafDatums,
+										   leafIsnulls);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1217,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1910,87 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * leaf key column datum of a leaf from the input datum. Otherwise, store
+	 * the input datum as is. Since we don't use index_form_tuple in this AM,
+	 * we have to make sure value to be inserted is not toasted;
+	 * FormIndexDatum doesn't guarantee that.  But we assume the "compress"
+	 * method to return an untoasted value.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[spgKeyColumn] = FunctionCall1Coll(compressProcinfo,
+														index->rd_indcollation[0],
+														datum[spgKeyColumn]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[spgKeyColumn] = PointerGetDatum(PG_DETOAST_DATUM(datum[spgKeyColumn]));
 			else
-				leafDatum = datum;
+				leafDatum[spgKeyColumn] = datum[spgKeyColumn];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[spgKeyColumn] = (Datum) 0;
+
+	for (i = spgFirstIncludeColumn; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->tupleDescriptor, i)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+						spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+		heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+	leafSize = leafSize < SGDTSIZE ? SGDTSIZE : MAXALIGN(leafSize);
+	leafSize += sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2002,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[spgKeyColumn] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2036,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[spgKeyColumn] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2078,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[spgKeyColumn] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2095,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[spgKeyColumn], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2109,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[spgKeyColumn]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[spgKeyColumn], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2151,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[spgKeyColumn];
+			in.leafDatum = leafDatum[spgKeyColumn];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2162,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[spgKeyColumn])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2199,18 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[spgKeyColumn])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[spgKeyColumn] = out.result.matchNode.restDatum;
+						leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+											spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+							heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+
+						/*
+						 * check for leafSize < SGDTSIZE is not needed for
+						 * non-null datum
+						 */
+						leafSize = MAXALIGN(leafSize) + sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index 0ca621450e..eeba97328e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -227,7 +226,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 20e67c3f7d..0ef1bcafd1 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,28 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are INCLUDE attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.tupleDescriptor->natts > 1)
+	{
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +523,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +550,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +565,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +760,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +774,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple);
 }
 
 /*
@@ -782,8 +806,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +901,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +928,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +973,44 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.tupleDescriptor->natts > 1)
+		{
+			/*
+			 * A general case of one key attribute and several INCLUDE
+			 * attributes
+			 */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.tupleDescriptor->natts));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.tupleDescriptor->natts));
+
+			spgDeformLeafTuple(leafTuple, so->state.tupleDescriptor, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * Override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[spgKeyColumn] = leafValue;
+			leafIsnulls[spgKeyColumn] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			/*
+			 * A particular case of no include attributes works exactly like
+			 * more general case above but don't make redundant allocations
+			 * and rewrites when there is only one attribute.
+			 */
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1078,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* INCLUDE attributes can always be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index d8b1815061..e0284d50eb 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,11 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
-
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -57,7 +61,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -105,6 +109,8 @@ SpGistCache *
 spgGetCache(Relation index)
 {
 	SpGistCache *cache;
+	int			i;
+	int			natts = IndexRelationGetNumberOfAttributes(index);
 
 	if (index->rd_amcache == NULL)
 	{
@@ -113,18 +119,42 @@ spgGetCache(Relation index)
 		FmgrInfo   *procinfo;
 		Buffer		metabuffer;
 		SpGistMetaPageData *metadata;
+		TupleDesc	tmpTupleDescriptor;
+
+		/*
+		 * SPGiST should have one key column and can also have INCLUDE columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
+		if (natts >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							natts, INDEX_MAX_KEYS)));
+
+		/* Form a tuple descriptor for all columns */
+		tmpTupleDescriptor = CreateTemplateTupleDesc(natts);
+
+		for (i = spgKeyColumn; i < natts; i++)
+			TupleDescInitEntry(tmpTupleDescriptor, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i)->atttypid, -1, 0);
 
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
-									   sizeof(SpGistCache));
+									   offsetof(struct SpGistCache, tupleDescriptor) +
+									   TupleDescSize(tmpTupleDescriptor));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		memcpy(&cache->tupleDescriptor, tmpTupleDescriptor,
+			   TupleDescSize(tmpTupleDescriptor));
+		pfree(tmpTupleDescriptor);
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -196,6 +226,7 @@ initSpGistState(SpGistState *state, Relation index)
 	state->attLeafType = cache->attLeafType;
 	state->attPrefixType = cache->attPrefixType;
 	state->attLabelType = cache->attLabelType;
+	state->tupleDescriptor = &cache->tupleDescriptor;
 
 	/* Make workspace for constructing dead tuples */
 	state->deadTupleStorage = palloc0(SGDTSIZE);
@@ -205,6 +236,7 @@ initSpGistState(SpGistState *state, Relation index)
 
 	/* Assume we're not in an index build (spgbuild will override) */
 	state->isBuild = false;
+
 }
 
 /*
@@ -604,8 +636,8 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
- * Also, we follow the SPGiST convention that pass-by-val types are
+ * Note the result is not maxaligned and this should be done by the caller if
+ * needed. Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
 unsigned int
@@ -620,7 +652,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +674,85 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 	}
 }
 
+Size
+spgNullmaskSize(int natts, bool *isnull)
+{
+	int			i;
+
+	Assert(natts > 0);
+	Assert(natts <= INDEX_MAX_KEYS);
+
+	/*
+	 * If there is only a key attribute (natts == 1), nullmask will not be
+	 * inserted (even inside the tuples, which have NULL key value). This
+	 * ensures compatibility with the previous versions tuple layout.
+	 */
+	if (natts == 1)
+		return 0;
+
+	for (i = spgKeyColumn; i < natts; i++)
+	{
+		/*
+		 * If there is at least one null, then nullmask will be sized to
+		 * contain a key attribute and all INCLUDE attributes.
+		 */
+		if (isnull[i])
+			return ((natts - 1) / 8) + 1;
+	}
+	return 0;
+}
+
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Construct a leaf tuple containing the given heap TID, datums and isnulls arrays.
+ * Nullmask apply only to INCLUDE attribute and is placed just after header if
+ * there is at least one NULL among INCLUDE attributes. It doesn't need alignment.
+ * Then all attributes data follow starting from MAXALIGN.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
-
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	TupleDesc	tupleDescriptor = state->tupleDescriptor;
+	uint16		tupmask = 0;
+	Size		data_size = heap_compute_data_size(tupleDescriptor, datum, isnull);
+	Size		nullmask_size = spgNullmaskSize(tupleDescriptor->natts, isnull);
+	Size		hoff = MAXALIGN(sizeof(SpGistLeafTupleData) + nullmask_size);
+	Size		size = MAXALIGN(hoff + data_size);
+	char	   *tp;				/* ptr to tuple data */
 
 	/*
-	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * Ensure that we can replace the tuple with a dead tuple later. This test
+	 * is unnecessary when !isnull[spgKeyColumn], but let's be safe.
 	 */
-	if (size < SGDTSIZE)
-		size = SGDTSIZE;
+	size = size < SGDTSIZE ? SGDTSIZE : MAXALIGN(size);
 
-	/* OK, form the tuple */
 	tup = (SpGistLeafTuple) palloc0(size);
-
 	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	SGLT_SET_OFFSET(tup, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
+	tp = (char *) tup + hoff;
+
+	if (nullmask_size)
+	{
+		bits8	   *bp;			/* ptr to null bitmap in tuple */
+
+		/* Set nullmask presence bit in SpGistLeafTuple header if needed */
+		SGLT_SET_CONTAINSNULLMASK(tup, true);
+		/* Fill nullmask and data part of a tuple */
+		bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData));
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask, bp);
+	}
+	else if (tupleDescriptor->natts > 1 || isnull[0] == false)
+
+		/*
+		 * Prevent filling nullmask in the tuple in case there should be no
+		 * nullmask.
+		 */
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask,
+						(bits8 *) NULL);
+
+	/* Single-column tuple with NULL value doesn't need filling data portion */
 
 	return tup;
 }
@@ -689,10 +770,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -736,7 +817,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -815,7 +896,7 @@ spgFormDeadTuple(SpGistState *state, int tupstate,
 
 	tuple->tupstate = tupstate;
 	tuple->size = SGDTSIZE;
-	tuple->nextOffset = InvalidOffsetNumber;
+	tuple->t_info = InvalidOffsetNumber;
 
 	if (tupstate == SPGIST_REDIRECT)
 	{
@@ -1047,3 +1128,49 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ */
+void
+spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+				   Datum *values, bool *isnull, bool keyColumnIsNull)
+{
+	bool		hasNullsMask = SGLT_GET_CONTAINSNULLMASK(tup);
+	char	   *tp;				/* ptr to tuple data */
+	bits8	   *bp;				/* ptr to null bitmap in tuple */
+
+	if (keyColumnIsNull && tupleDescriptor->natts == 1)
+	{
+		/*
+		 * Trivial case: there is only key attribute and we're in a nulls
+		 * tree. hasNullsMask bit in a tuple header should not be set for
+		 * single attribute case even if it has NULL value (for compatibility
+		 * with pre-v14 SpGist tuple format) We should not call
+		 * index_deform_anyheader_tuple() in this trivial case as it expects
+		 * nullmask in a tuple present in this case.
+		 */
+		Assert(hasNullsMask == 0);
+
+		isnull[spgKeyColumn] = true;
+		values[spgKeyColumn] = (Datum) 0;
+		return;
+	}
+	else if (hasNullsMask)
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData) +
+									 ((tupleDescriptor->natts - 1) / 8 + 1));
+	else
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData));
+
+	bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData));
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp, hasNullsMask);
+
+	/*
+	 * Key column isnull value from a tuple should be consistent with
+	 * keyColumnIsNull got from the caller
+	 */
+	Assert(keyColumnIsNull == isnull[spgKeyColumn]);
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index a9ffca5183..684efa7cd6 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt) > max ||
+					predecessor[SGLT_GET_OFFSET(lt)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits have their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index d40c7b5877..a1d8686907 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head) == SGLT_GET_OFFSET(&leafTupleHdr));
+				SGLT_SET_OFFSET(head, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/itup.h b/src/include/access/itup.h
index b6813707d0..09d947a996 100644
--- a/src/include/access/itup.h
+++ b/src/include/access/itup.h
@@ -154,6 +154,9 @@ extern Datum nocache_index_getattr(IndexTuple tup, int attnum,
 								   TupleDesc tupleDesc);
 extern void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
 							   Datum *values, bool *isnull);
+extern void index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+										 Datum *values, bool *isnull,
+										 bits8 *bp, char *tp, bool hasnulls);
 extern IndexTuple CopyIndexTuple(IndexTuple source);
 extern IndexTuple index_truncate_tuple(TupleDesc sourceDescriptor,
 									   IndexTuple source, int leavenatts);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index a81bab24ea..530f8cccd8 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,13 +22,15 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 } SpGistOptions;
 
+#define spgKeyColumn 0
+#define spgFirstIncludeColumn 1
+
 #define SpGistGetFillFactor(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == SPGIST_AM_OID), \
@@ -146,28 +148,9 @@ typedef struct SpGistState
 
 	TransactionId myXid;		/* XID to use when creating a redirect tuple */
 	bool		isBuild;		/* true if doing index build */
+	TupleDesc	tupleDescriptor;	/* tuple descriptor */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -243,9 +226,9 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLabelType;	/* type of node label values */
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+	TupleDescData tupleDescriptor;	/* descriptor for leaf tuples */
 } SpGistCache;
 
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +288,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,23 +305,21 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a heap tuple TID and columns datums and
+ * nullmasks.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of INCLUDE columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
  * wide to be stored immediately, and it costs nothing because of alignment
  * considerations.
  *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
  * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
+ * so that the tuple can be converted to REDIRECT status later. (This
  * restriction only adds bytes for the null-datum case, otherwise alignment
  * restrictions force it anyway.)
  *
@@ -346,23 +327,65 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Normally, nextOffset inside t_info links to the next tuple belonging to
+ * the same parent node (which must be on the same page).  But when the root
+ * page is a leaf page, we don't chain its tuples, so nextOffset is always 0
+ * on the root. Minimum space to store SpGistLeafTuple plus ItemIdData on a
+ * page is 16 bytes, so 15 lower bits for nextOffset is enough to store tuple
+ * number in a chain on a page even if a page size is 64Kb.
+ *
+ * The highest bit in t_info is to store per-tuple information is there nulls
+ * mask exist for the case there are INCLUDE attributes. If there are no
+ * INCLUDE columns this bit is set to 0 and nullmask is not added even if it
+ * is an empty tuple with NULL key value.
+ *
+ * Datums for all columns are stored in ordinary index-tuple-like way starting
+ * from MAXALIGN boundary. Nullmask with size (number of columns)/8
+ * bytes is put without alignment just after the ending of tuple header.
+ * On 64-bit architecture nullmask has a good chance to fit into the alignment
+ * gap between the header and the first datum, thus making its storage free
+ * of charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * t_info is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: values has nulls
+	 * 14-0 bit: nextOffset i.e. number of next tuple in chain on a page,
+	 * 			 or InvalidOffsetNumber
+	 * ---------------
+	 */
+	unsigned short t_info;		/* nextOffset for linking tuples in a chain on
+								 * a leaf page, and additional info */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* nullmask follows if there are nulls among attributes */
+	/* attributes data follow starting from MAXALIGN */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
 
 #define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
 #define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
+#define SGLTDATUM(x, s)		fetch_att(SGLTDATAPTR(x), (s)->attLeafType.attbyval, \
+							(s)->attLeafType.attlen)
+/*
+ * Macros to access nextOffset and bit fields inside t_info independently.
+ */
+#define SGLT_GET_OFFSET(spgLeafTuple)	( (spgLeafTuple)->t_info & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(spgLeafTuple) \
+	( (bool)((spgLeafTuple)->t_info >> 15) )
+#define SGLT_SET_OFFSET(spgLeafTuple, offsetNumber) \
+	( (spgLeafTuple)->t_info = \
+	((spgLeafTuple)->t_info & 0xC000) | ((offsetNumber) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(spgLeafTuple, is_null) \
+	( (spgLeafTuple)->t_info = \
+	((uint16)(bool)(is_null) << 15) | ((spgLeafTuple)->t_info & 0x3FFF) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -372,14 +395,14 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
  * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
  * field, to satisfy some Asserts that we make when replacing a leaf tuple
  * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
+ * We don't use t_info, but it's needed to align the pointer field.
  * pointer and xid are only valid when tupstate = REDIRECT.
  */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;
-	OffsetNumber nextOffset;	/* not used in dead tuples */
+	unsigned short t_info;		/* not used in dead tuples */
 	ItemPointerData pointer;	/* redirection inside index */
 	TransactionId xid;			/* ID of xact that inserted this tuple */
 } SpGistDeadTupleData;
@@ -394,7 +417,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +432,27 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+
 /*
  * XLOG stuff
  */
@@ -456,9 +499,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern Size spgNullmaskSize(int natts, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +510,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+							   Datum *datum, bool *isnull, bool keyIsNull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +530,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..7ab6113c61 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -171,7 +171,7 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  spgist | can_unique    | f
  spgist | can_multi_col | f
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..86510687c7 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -349,14 +349,13 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 
 DROP TABLE tbl;
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b3605db88c..5fa57d4201 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -52,7 +52,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 15ec7548e3..d89d21187b 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..44b340053b 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -182,7 +182,7 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 DROP TABLE tbl;
 
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
-- 
2.28.0

#29Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Pavel Borisov (#28)
1 attachment(s)
Re: [PATCH] Covering SPGiST index

In a v14 I forgot to add the test. PFA v15

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

v15-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchapplication/octet-stream; name=v15-0001-Covering-SP-GiST-index-support-for-INCLUDE-colum.patchDownload
From 7508b30497e5fbf88e9eada611c08bdefe1c634c Mon Sep 17 00:00:00 2001
From: Pavel Borisov <pashkin.elfe@gmail.com>
Date: Thu, 25 Mar 2021 23:32:58 +0400
Subject: [PATCH v15] Covering SP-GiST index - support for INCLUDE columns

Adding INCLUDE columns for SPGiST index is intended to increase the speed of queries by making scans index-only likewise
in btree and GiST index. These columns are added only to leaf tuples and they are not used in index tree search but they
can be fetched during index scan.

The other point of INCLUDE columns is to overcome SP-GiST limitation of being single-column in principle. I.e. in certain
cases a single covering SP-GiST index can replace several separate ones with less disk space and shared buffers
consumption, faster, update etc. Also, any data types without SP-GiST supported opclasses can be included.

Discussion: https://www.postgresql.org/message-id/flat/CALT9ZEFi-vMp4faht9f9Junb1nO3NOSjhpxTmbm1UGLMsLqiEQ@mail.gmail.com
---
 doc/src/sgml/indices.sgml                     |   4 +-
 doc/src/sgml/ref/create_index.sgml            |   4 +-
 doc/src/sgml/spgist.sgml                      |   8 +
 src/backend/access/common/indextuple.c        |  46 +++--
 src/backend/access/spgist/README              |  17 ++
 src/backend/access/spgist/spgdoinsert.c       | 174 ++++++++++------
 src/backend/access/spgist/spginsert.c         |   5 +-
 src/backend/access/spgist/spgscan.c           |  91 +++++++--
 src/backend/access/spgist/spgutils.c          | 191 +++++++++++++++---
 src/backend/access/spgist/spgvacuum.c         |  25 ++-
 src/backend/access/spgist/spgxlog.c           |   6 +-
 src/include/access/itup.h                     |   3 +
 src/include/access/spgist_private.h           | 128 ++++++++----
 src/test/regress/expected/amutils.out         |   2 +-
 src/test/regress/expected/index_including.out |   3 +-
 .../expected/index_including_spgist.out       | 143 +++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/serial_schedule              |   1 +
 src/test/regress/sql/index_including.sql      |   2 +-
 .../regress/sql/index_including_spgist.sql    |  84 ++++++++
 20 files changed, 751 insertions(+), 188 deletions(-)
 create mode 100644 src/test/regress/expected/index_including_spgist.out
 create mode 100644 src/test/regress/sql/index_including_spgist.sql

diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml
index 623962d1d8..7a6bb03bef 100644
--- a/doc/src/sgml/indices.sgml
+++ b/doc/src/sgml/indices.sgml
@@ -1208,8 +1208,8 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);
    likely to not need to access the heap.  If the heap tuple must be visited
    anyway, it costs nothing more to get the column's value from there.
    Other restrictions are that expressions are not currently supported as
-   included columns, and that only B-tree and GiST indexes currently support
-   included columns.
+   included columns, and that only B-tree, GiST and SP-GiST indexes currently
+   support included columns.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml
index 682af4bbe4..cb74c998b1 100644
--- a/doc/src/sgml/ref/create_index.sgml
+++ b/doc/src/sgml/ref/create_index.sgml
@@ -187,8 +187,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] <replaceable class=
        </para>
 
        <para>
-        Currently, the B-tree and the GiST index access methods support this
-        feature.  In B-tree and the GiST indexes, the values of columns listed
+        Currently, the B-tree, GiST and SP-GiST index access methods support
+        this feature.  In these indexes, the values of columns listed
         in the <literal>INCLUDE</literal> clause are included in leaf tuples
         which correspond to heap tuples, but are not included in upper-level
         index entries used for tree navigation.
diff --git a/doc/src/sgml/spgist.sgml b/doc/src/sgml/spgist.sgml
index ea88ae45e5..148b5629b7 100644
--- a/doc/src/sgml/spgist.sgml
+++ b/doc/src/sgml/spgist.sgml
@@ -214,6 +214,14 @@
   inner tuples that are passed through to reach the leaf level.
  </para>
 
+ <para>
+  In case when <acronym>SP-GiST</acronym> index is created with
+  <literal>INCLUDE</literal> clause i.e. covering index, leaf tuples also
+  contain data from included columns. This data is stored uncompressed and can have
+  data types without any SP-GiST operator class.
+
+ </para>
+
  <para>
   Inner tuples are more complex, since they are branching points in the
   search tree.  Each inner tuple contains a set of one or more
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1f6b7b77d4..f4819fbed0 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -422,35 +422,55 @@ nocache_index_getattr(IndexTuple tup,
 	return fetchatt(TupleDescAttr(tupleDesc, attnum), tp + off);
 }
 
+/* Convert index tuple into Datum/isnull arrays */
+void
+index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
+				   Datum *values, bool *isnull)
+{
+	bits8	   *bp;				/* ptr to null bitmap in tuple */
+	char	   *tp;				/* ptr to tuple data */
+
+	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp,
+								 (bool) IndexTupleHasNulls(tup));
+}
+
 /*
- * Convert an index tuple into Datum/isnull arrays.
+ * Convert an index-like tuples but with arbitrary header length into
+ * Datum/isnull arrays.
  *
  * The caller must allocate sufficient storage for the output arrays.
  * (INDEX_MAX_KEYS entries should be enough.)
  *
- * This is nearly the same as heap_deform_tuple(), but for IndexTuples.
- * One difference is that the tuple should never have any missing columns.
+ * This is nearly the same as heap_deform_tuple(), but for IndexTuples and
+ * SpGistLeafTuples. One difference is that the tuple should never have any
+ * missing columns.
+ *
+ * Callers are expected to provide pointer to null bitmap, MAXALIGN-ed
+ * pointer to tuple data and hasnulls bit got from the header.
  */
 void
-index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
-				   Datum *values, bool *isnull)
+index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+							 Datum *values, bool *isnull,
+							 bits8 *bp, char *tp, bool hasnulls)
 {
-	int			hasnulls = IndexTupleHasNulls(tup);
 	int			natts = tupleDescriptor->natts; /* number of atts to extract */
 	int			attnum;
-	char	   *tp;				/* ptr to tuple data */
-	int			off;			/* offset in tuple data */
-	bits8	   *bp;				/* ptr to null bitmap in tuple */
+	int			off = 0;		/* offset in tuple data */
 	bool		slow = false;	/* can we use/set attcacheoff? */
 
 	/* Assert to protect callers who allocate fixed-size arrays */
 	Assert(natts <= INDEX_MAX_KEYS);
 
-	/* XXX "knows" t_bits are just after fixed tuple header! */
-	bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData));
+	/* If has hulls, null mask should be present in a tuple */
+	Assert(hasnulls == false || ((char *) bp < tp));
 
-	tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info);
-	off = 0;
+	/* Tuple data should start from MAXALIGN */
+	Assert(tp == (char *) MAXALIGN(tp));
 
 	for (attnum = 0; attnum < natts; attnum++)
 	{
diff --git a/src/backend/access/spgist/README b/src/backend/access/spgist/README
index b55b073832..ab5ff3d434 100644
--- a/src/backend/access/spgist/README
+++ b/src/backend/access/spgist/README
@@ -76,6 +76,19 @@ Leaf tuple consists of:
 
   ItemPointer to the heap
 
+  nextOffset number of next leaf tuple in a chain on a leaf page
+  optional nullmask
+  optional INCLUDE columns values
+
+Leaf tuple layout changed since PostgreSQL version 14 to support INCLUDE
+columns but in a way that doesn't change the header and the key value
+placement for a tuple without INCLUDE columns. So indexes created earlier
+remain fully supported.
+
+Nullmask is added only if there are INCLUDE columns and nulls in a tuple.
+It is added without alignment and 64-bit architectures has a good chance
+to fit alignment gap before first value which makes its storage free of
+charge.
 
 NULLS HANDLING
 
@@ -90,6 +103,10 @@ Insertions and searches in the nulls tree do not use any of the
 opclass-supplied functions, but just use hardwired logic comparable to
 AllTheSame cases in the normal tree.
 
+For INCLUDE attributes nulls are handled in ordinary per leaf-tuple way i.e.
+if null mask presence bit in a header is set, nullmask is added to tuple.
+If there is nullmask it covers all attributes, key attribute as well. It is
+redundant but allows to use index tuple code to operate SpGistLeafTuple.
 
 INSERTION ALGORITHM
 
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 7bd269fd2a..01a0ee0921 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -22,7 +22,7 @@
 #include "miscadmin.h"
 #include "storage/bufmgr.h"
 #include "utils/rel.h"
-
+#include "access/htup_details.h"
 
 /*
  * SPPageDesc tracks all info about a page we are inserting into.  In some
@@ -220,7 +220,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 		SpGistBlockIsRoot(current->blkno))
 	{
 		/* Tuple is not part of a chain */
-		leafTuple->nextOffset = InvalidOffsetNumber;
+		SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 		current->offnum = SpGistPageAddNewItem(state, current->page,
 											   (Item) leafTuple, leafTuple->size,
 											   NULL, false);
@@ -253,7 +253,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 											 PageGetItemId(current->page, current->offnum));
 		if (head->tupstate == SPGIST_LIVE)
 		{
-			leafTuple->nextOffset = head->nextOffset;
+			SGLT_SET_OFFSET(leafTuple, SGLT_GET_OFFSET(head));
 			offnum = SpGistPageAddNewItem(state, current->page,
 										  (Item) leafTuple, leafTuple->size,
 										  NULL, false);
@@ -264,14 +264,14 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 			 */
 			head = (SpGistLeafTuple) PageGetItem(current->page,
 												 PageGetItemId(current->page, current->offnum));
-			head->nextOffset = offnum;
+			SGLT_SET_OFFSET(head, offnum);
 
 			xlrec.offnumLeaf = offnum;
 			xlrec.offnumHeadLeaf = current->offnum;
 		}
 		else if (head->tupstate == SPGIST_DEAD)
 		{
-			leafTuple->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(leafTuple, InvalidOffsetNumber);
 			PageIndexTupleDelete(current->page, current->offnum);
 			if (PageAddItem(current->page,
 							(Item) leafTuple, leafTuple->size,
@@ -362,13 +362,13 @@ checkSplitConditions(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* Don't count it in result, because it won't go to other page */
 		}
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	*nToSplit = n;
@@ -437,7 +437,7 @@ moveLeafs(Relation index, SpGistState *state,
 		{
 			/* We could see a DEAD tuple as first/only chain item */
 			Assert(i == current->offnum);
-			Assert(it->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 			/* We don't want to move it, so don't count it in size */
 			toDelete[nDelete] = i;
 			nDelete++;
@@ -446,7 +446,7 @@ moveLeafs(Relation index, SpGistState *state,
 		else
 			elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-		i = it->nextOffset;
+		i = SGLT_GET_OFFSET(it);
 	}
 
 	/* Find a leaf page that will hold them */
@@ -475,7 +475,7 @@ moveLeafs(Relation index, SpGistState *state,
 			 * don't care).  We're modifying the tuple on the source page
 			 * here, but it's okay since we're about to delete it.
 			 */
-			it->nextOffset = r;
+			SGLT_SET_OFFSET(it, r);
 
 			r = SpGistPageAddNewItem(state, npage, (Item) it, it->size,
 									 &startOffset, false);
@@ -490,7 +490,7 @@ moveLeafs(Relation index, SpGistState *state,
 	}
 
 	/* add the new tuple as well */
-	newLeafTuple->nextOffset = r;
+	SGLT_SET_OFFSET(newLeafTuple, r);
 	r = SpGistPageAddNewItem(state, npage,
 							 (Item) newLeafTuple, newLeafTuple->size,
 							 &startOffset, false);
@@ -690,13 +690,13 @@ doPickSplit(Relation index, SpGistState *state,
 			   *nodes;
 	Buffer		newInnerBuffer,
 				newLeafBuffer;
-	ItemPointerData *heapPtrs;
 	uint8	   *leafPageSelect;
 	int		   *leafSizes;
 	OffsetNumber *toDelete;
 	OffsetNumber *toInsert;
 	OffsetNumber redirectTuplePos = InvalidOffsetNumber;
 	OffsetNumber startOffsets[2];
+	SpGistLeafTuple *oldLeafs;
 	SpGistLeafTuple *newLeafs;
 	int			spaceToDelete;
 	int			currentFreeSpace;
@@ -709,6 +709,8 @@ doPickSplit(Relation index, SpGistState *state,
 	int			nToDelete,
 				nToInsert,
 				maxToInclude;
+	Datum		leafDatums[INDEX_MAX_KEYS];
+	bool		leafIsnulls[INDEX_MAX_KEYS];
 
 	in.level = level;
 
@@ -718,12 +720,11 @@ doPickSplit(Relation index, SpGistState *state,
 	max = PageGetMaxOffsetNumber(current->page);
 	n = max + 1;
 	in.datums = (Datum *) palloc(sizeof(Datum) * n);
-	heapPtrs = (ItemPointerData *) palloc(sizeof(ItemPointerData) * n);
 	toDelete = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
 	toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n);
+	oldLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n);
 	leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n);
-
 	STORE_STATE(state, xlrec.stateSrc);
 
 	/*
@@ -739,6 +740,7 @@ doPickSplit(Relation index, SpGistState *state,
 	 * we are just computing a pointer that isn't going to get dereferenced.
 	 * So it's not worth guarding the calls with isNulls checks.
 	 */
+
 	nToInsert = 0;
 	nToDelete = 0;
 	spaceToDelete = 0;
@@ -758,7 +760,7 @@ doPickSplit(Relation index, SpGistState *state,
 			if (it->tupstate == SPGIST_LIVE)
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
-				heapPtrs[nToInsert] = it->heapPtr;
+				oldLeafs[nToInsert] = it;
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -783,7 +785,7 @@ doPickSplit(Relation index, SpGistState *state,
 			if (it->tupstate == SPGIST_LIVE)
 			{
 				in.datums[nToInsert] = SGLTDATUM(it, state);
-				heapPtrs[nToInsert] = it->heapPtr;
+				oldLeafs[nToInsert] = it;
 				nToInsert++;
 				toDelete[nToDelete] = i;
 				nToDelete++;
@@ -795,7 +797,7 @@ doPickSplit(Relation index, SpGistState *state,
 			{
 				/* We could see a DEAD tuple as first/only chain item */
 				Assert(i == current->offnum);
-				Assert(it->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(it) == InvalidOffsetNumber);
 				toDelete[nToDelete] = i;
 				nToDelete++;
 				/* replacing it with redirect will save no space */
@@ -803,7 +805,7 @@ doPickSplit(Relation index, SpGistState *state,
 			else
 				elog(ERROR, "unexpected SPGiST tuple state: %d", it->tupstate);
 
-			i = it->nextOffset;
+			i = SGLT_GET_OFFSET(it);
 		}
 	}
 	in.nTuples = nToInsert;
@@ -815,7 +817,7 @@ doPickSplit(Relation index, SpGistState *state,
 	 * for the picksplit function.  So don't increment nToInsert yet.
 	 */
 	in.datums[in.nTuples] = SGLTDATUM(newLeafTuple, state);
-	heapPtrs[in.nTuples] = newLeafTuple->heapPtr;
+	oldLeafs[in.nTuples] = newLeafTuple;
 	in.nTuples++;
 
 	memset(&out, 0, sizeof(out));
@@ -837,9 +839,17 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   out.leafTupleDatums[i],
-										   false);
+			spgDeformLeafTuple(oldLeafs[i], state->tupleDescriptor,
+							   leafDatums,
+							   leafIsnulls,
+							   isNulls);
+
+			leafDatums[spgKeyColumn] = (Datum) out.leafTupleDatums[i];
+			leafIsnulls[spgKeyColumn] = false;
+
+			newLeafs[i] = spgFormLeafTuple(state, &oldLeafs[i]->heapPtr,
+										   leafDatums,
+										   leafIsnulls);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -860,9 +870,20 @@ doPickSplit(Relation index, SpGistState *state,
 		totalLeafSizes = 0;
 		for (i = 0; i < in.nTuples; i++)
 		{
-			newLeafs[i] = spgFormLeafTuple(state, heapPtrs + i,
-										   (Datum) 0,
-										   true);
+			spgDeformLeafTuple(oldLeafs[i], state->tupleDescriptor,
+							   leafDatums,
+							   leafIsnulls,
+							   isNulls);
+
+			/*
+			 * Nulls tree can contain only null key values.
+			 */
+			leafDatums[spgKeyColumn] = (Datum) 0;
+			leafIsnulls[spgKeyColumn] = true;
+
+			newLeafs[i] = spgFormLeafTuple(state, &oldLeafs[i]->heapPtr,
+										   leafDatums,
+										   leafIsnulls);
 			totalLeafSizes += newLeafs[i]->size + sizeof(ItemIdData);
 		}
 	}
@@ -1196,10 +1217,10 @@ doPickSplit(Relation index, SpGistState *state,
 		if (ItemPointerIsValid(&nodes[n]->t_tid))
 		{
 			Assert(ItemPointerGetBlockNumber(&nodes[n]->t_tid) == leafBlock);
-			it->nextOffset = ItemPointerGetOffsetNumber(&nodes[n]->t_tid);
+			SGLT_SET_OFFSET(it, ItemPointerGetOffsetNumber(&nodes[n]->t_tid));
 		}
 		else
-			it->nextOffset = InvalidOffsetNumber;
+			SGLT_SET_OFFSET(it, InvalidOffsetNumber);
 
 		/* Insert it on page */
 		newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer),
@@ -1889,67 +1910,87 @@ spgSplitNodeAction(Relation index, SpGistState *state,
  */
 bool
 spgdoinsert(Relation index, SpGistState *state,
-			ItemPointer heapPtr, Datum datum, bool isnull)
+			ItemPointer heapPtr, Datum *datum, bool *isnull)
 {
 	int			level = 0;
-	Datum		leafDatum;
+	Datum	   *leafDatum;
 	int			leafSize;
 	SPPageDesc	current,
 				parent;
 	FmgrInfo   *procinfo = NULL;
+	int			i;
 
 	/*
 	 * Look up FmgrInfo of the user-defined choose function once, to save
 	 * cycles in the loop below.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 		procinfo = index_getprocinfo(index, 1, SPGIST_CHOOSE_PROC);
 
 	/*
 	 * Prepare the leaf datum to insert.
-	 *
+	 */
+
+	leafDatum = (Datum *) palloc0(sizeof(Datum) * (IndexRelationGetNumberOfAttributes(index)));
+
+	/*
 	 * If an optional "compress" method is provided, then call it to form the
-	 * leaf datum from the input datum.  Otherwise store the input datum as
-	 * is.  Since we don't use index_form_tuple in this AM, we have to make
-	 * sure value to be inserted is not toasted; FormIndexDatum doesn't
-	 * guarantee that.  But we assume the "compress" method to return an
-	 * untoasted value.
+	 * leaf key column datum of a leaf from the input datum. Otherwise, store
+	 * the input datum as is. Since we don't use index_form_tuple in this AM,
+	 * we have to make sure value to be inserted is not toasted;
+	 * FormIndexDatum doesn't guarantee that.  But we assume the "compress"
+	 * method to return an untoasted value.
 	 */
-	if (!isnull)
+	if (!isnull[spgKeyColumn])
 	{
 		if (OidIsValid(index_getprocid(index, 1, SPGIST_COMPRESS_PROC)))
 		{
 			FmgrInfo   *compressProcinfo = NULL;
 
 			compressProcinfo = index_getprocinfo(index, 1, SPGIST_COMPRESS_PROC);
-			leafDatum = FunctionCall1Coll(compressProcinfo,
-										  index->rd_indcollation[0],
-										  datum);
+			leafDatum[spgKeyColumn] = FunctionCall1Coll(compressProcinfo,
+														index->rd_indcollation[0],
+														datum[spgKeyColumn]);
 		}
 		else
 		{
 			Assert(state->attLeafType.type == state->attType.type);
 
 			if (state->attType.attlen == -1)
-				leafDatum = PointerGetDatum(PG_DETOAST_DATUM(datum));
+				leafDatum[spgKeyColumn] = PointerGetDatum(PG_DETOAST_DATUM(datum[spgKeyColumn]));
 			else
-				leafDatum = datum;
+				leafDatum[spgKeyColumn] = datum[spgKeyColumn];
 		}
 	}
 	else
-		leafDatum = (Datum) 0;
+		leafDatum[spgKeyColumn] = (Datum) 0;
+
+	for (i = spgFirstIncludeColumn; i < IndexRelationGetNumberOfAttributes(index); i++)
+	{
+		if (!isnull[i])
+		{
+			if (TupleDescAttr(state->tupleDescriptor, i)->attlen == -1)
+				leafDatum[i] = PointerGetDatum(PG_DETOAST_DATUM(datum[i]));
+			else
+				leafDatum[i] = datum[i];
+		}
+		else
+			leafDatum[i] = (Datum) 0;
+	}
+
 
 	/*
-	 * Compute space needed for a leaf tuple containing the given datum.
+	 * Compute space needed on a page for a leaf tuple containing the given
+	 * datum.
 	 *
 	 * If it isn't gonna fit, and the opclass can't reduce the datum size by
 	 * suffixing, bail out now rather than getting into an endless loop.
 	 */
-	if (!isnull)
-		leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-			SpGistGetTypeSize(&state->attLeafType, leafDatum);
-	else
-		leafSize = SGDTSIZE + sizeof(ItemIdData);
+	leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+						spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+		heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+	leafSize = leafSize < SGDTSIZE ? SGDTSIZE : MAXALIGN(leafSize);
+	leafSize += sizeof(ItemIdData);
 
 	if (leafSize > SPGIST_PAGE_CAPACITY && !state->config.longValuesOK)
 		ereport(ERROR,
@@ -1961,7 +2002,7 @@ spgdoinsert(Relation index, SpGistState *state,
 				 errhint("Values larger than a buffer page cannot be indexed.")));
 
 	/* Initialize "current" to the appropriate root page */
-	current.blkno = isnull ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
+	current.blkno = isnull[spgKeyColumn] ? SPGIST_NULL_BLKNO : SPGIST_ROOT_BLKNO;
 	current.buffer = InvalidBuffer;
 	current.page = NULL;
 	current.offnum = FirstOffsetNumber;
@@ -1995,7 +2036,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			 */
 			current.buffer =
 				SpGistGetBuffer(index,
-								GBUF_LEAF | (isnull ? GBUF_NULLS : 0),
+								GBUF_LEAF | (isnull[spgKeyColumn] ? GBUF_NULLS : 0),
 								Min(leafSize, SPGIST_PAGE_CAPACITY),
 								&isNew);
 			current.blkno = BufferGetBlockNumber(current.buffer);
@@ -2037,7 +2078,7 @@ spgdoinsert(Relation index, SpGistState *state,
 		current.page = BufferGetPage(current.buffer);
 
 		/* should not arrive at a page of the wrong type */
-		if (isnull ? !SpGistPageStoresNulls(current.page) :
+		if (isnull[spgKeyColumn] ? !SpGistPageStoresNulls(current.page) :
 			SpGistPageStoresNulls(current.page))
 			elog(ERROR, "SPGiST index page %u has wrong nulls flag",
 				 current.blkno);
@@ -2054,7 +2095,7 @@ spgdoinsert(Relation index, SpGistState *state,
 			{
 				/* it fits on page, so insert it and we're done */
 				addLeafTuple(index, state, leafTuple,
-							 &current, &parent, isnull, isNew);
+							 &current, &parent, isnull[spgKeyColumn], isNew);
 				break;
 			}
 			else if ((sizeToSplit =
@@ -2068,14 +2109,14 @@ spgdoinsert(Relation index, SpGistState *state,
 				 * chain to another leaf page rather than splitting it.
 				 */
 				Assert(!isNew);
-				moveLeafs(index, state, &current, &parent, leafTuple, isnull);
+				moveLeafs(index, state, &current, &parent, leafTuple, isnull[spgKeyColumn]);
 				break;			/* we're done */
 			}
 			else
 			{
 				/* picksplit */
 				if (doPickSplit(index, state, &current, &parent,
-								leafTuple, level, isnull, isNew))
+								leafTuple, level, isnull[spgKeyColumn], isNew))
 					break;		/* doPickSplit installed new tuples */
 
 				/* leaf tuple will not be inserted yet */
@@ -2110,8 +2151,8 @@ spgdoinsert(Relation index, SpGistState *state,
 			innerTuple = (SpGistInnerTuple) PageGetItem(current.page,
 														PageGetItemId(current.page, current.offnum));
 
-			in.datum = datum;
-			in.leafDatum = leafDatum;
+			in.datum = datum[spgKeyColumn];
+			in.leafDatum = leafDatum[spgKeyColumn];
 			in.level = level;
 			in.allTheSame = innerTuple->allTheSame;
 			in.hasPrefix = (innerTuple->prefixSize > 0);
@@ -2121,7 +2162,7 @@ spgdoinsert(Relation index, SpGistState *state,
 
 			memset(&out, 0, sizeof(out));
 
-			if (!isnull)
+			if (!isnull[spgKeyColumn])
 			{
 				/* use user-defined choose method */
 				FunctionCall2Coll(procinfo,
@@ -2158,11 +2199,18 @@ spgdoinsert(Relation index, SpGistState *state,
 					/* Adjust level as per opclass request */
 					level += out.result.matchNode.levelAdd;
 					/* Replace leafDatum and recompute leafSize */
-					if (!isnull)
+					if (!isnull[spgKeyColumn])
 					{
-						leafDatum = out.result.matchNode.restDatum;
-						leafSize = SGLTHDRSZ + sizeof(ItemIdData) +
-							SpGistGetTypeSize(&state->attLeafType, leafDatum);
+						leafDatum[spgKeyColumn] = out.result.matchNode.restDatum;
+						leafSize = MAXALIGN(sizeof(SpGistLeafTupleData) +
+											spgNullmaskSize(state->tupleDescriptor->natts, isnull)) +
+							heap_compute_data_size(state->tupleDescriptor, leafDatum, isnull);
+
+						/*
+						 * check for leafSize < SGDTSIZE is not needed for
+						 * non-null datum
+						 */
+						leafSize = MAXALIGN(leafSize) + sizeof(ItemIdData);
 					}
 
 					/*
@@ -2227,6 +2275,6 @@ spgdoinsert(Relation index, SpGistState *state,
 		SpGistSetLastUsedPage(index, parent.buffer);
 		UnlockReleaseBuffer(parent.buffer);
 	}
-
+	pfree(leafDatum);
 	return true;
 }
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index 0ca621450e..eeba97328e 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -55,8 +55,7 @@ spgistBuildCallback(Relation index, ItemPointer tid, Datum *values,
 	 * lock on some buffer.  So we need to be willing to retry.  We can flush
 	 * any temp data when retrying.
 	 */
-	while (!spgdoinsert(index, &buildstate->spgstate, tid,
-						*values, *isnull))
+	while (!spgdoinsert(index, &buildstate->spgstate, tid, values, isnull))
 	{
 		MemoryContextReset(buildstate->tmpCtx);
 	}
@@ -227,7 +226,7 @@ spginsert(Relation index, Datum *values, bool *isnull,
 	 * to avoid cumulative memory consumption.  That means we also have to
 	 * redo initSpGistState(), but it's cheap enough not to matter.
 	 */
-	while (!spgdoinsert(index, &spgstate, ht_ctid, *values, *isnull))
+	while (!spgdoinsert(index, &spgstate, ht_ctid, values, isnull))
 	{
 		MemoryContextReset(insertCtx);
 		initSpGistState(&spgstate, index);
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 20e67c3f7d..0ef1bcafd1 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
 
 typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
 							   Datum leafValue, bool isNull, bool recheck,
-							   bool recheckDistances, double *distances);
+							   bool recheckDistances, double *distances,
+							   SpGistLeafTuple leafTuple);
 
 /*
  * Pairing heap comparison function for the SpGistSearchItem queue.
@@ -88,6 +89,9 @@ spgFreeSearchItem(SpGistScanOpaque so, SpGistSearchItem *item)
 	if (item->traversalValue)
 		pfree(item->traversalValue);
 
+	if (item->isLeaf && item->leafTuple)
+		pfree(item->leafTuple);
+
 	pfree(item);
 }
 
@@ -134,6 +138,8 @@ spgAddStartItem(SpGistScanOpaque so, bool isnull)
 	startEntry->recheck = false;
 	startEntry->recheckDistances = false;
 
+	startEntry->leafTuple = NULL;
+
 	spgAddSearchItemToQueue(so, startEntry);
 }
 
@@ -438,14 +444,28 @@ spgendscan(IndexScanDesc scan)
  * Leaf SpGistSearchItem constructor, called in queue context
  */
 static SpGistSearchItem *
-spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
+spgNewHeapItem(SpGistScanOpaque so, int level, SpGistLeafTuple leafTuple,
 			   Datum leafValue, bool recheck, bool recheckDistances,
 			   bool isnull, double *distances)
 {
 	SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
 
+	/*
+	 * If there are INCLUDE attributes search item in the queue should contain
+	 * them.
+	 */
+	if (so->state.tupleDescriptor->natts > 1)
+	{
+		item->leafTuple = palloc(leafTuple->size);
+		memcpy(item->leafTuple, leafTuple, leafTuple->size);
+	}
+	else
+	{
+		item->leafTuple = NULL;
+	}
+
 	item->level = level;
-	item->heapPtr = *heapPtr;
+	item->heapPtr = leafTuple->heapPtr;
 	/* copy value to queue cxt out of tmp cxt */
 	item->value = isnull ? (Datum) 0 :
 		datumCopy(leafValue, so->state.attLeafType.attbyval,
@@ -503,6 +523,8 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		in.returnData = so->want_itup;
 		in.leafDatum = SGLTDATUM(leafTuple, &so->state);
 
+
+
 		out.leafValue = (Datum) 0;
 		out.recheck = false;
 		out.distances = NULL;
@@ -528,7 +550,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 			/* the scan is ordered -> add the item to the queue */
 			MemoryContext oldCxt = MemoryContextSwitchTo(so->traversalCxt);
 			SpGistSearchItem *heapItem = spgNewHeapItem(so, item->level,
-														&leafTuple->heapPtr,
+														leafTuple,
 														leafValue,
 														recheck,
 														recheckDistances,
@@ -543,8 +565,10 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
 		{
 			/* non-ordered scan, so report the item right away */
 			Assert(!recheckDistances);
+
 			storeRes(so, &leafTuple->heapPtr, leafValue, isnull,
-					 recheck, false, NULL);
+					 recheck, false, NULL, leafTuple);
+
 			*reportedSome = true;
 		}
 	}
@@ -736,7 +760,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 				/* dead tuple should be first in chain */
 				Assert(offset == ItemPointerGetOffsetNumber(&item->heapPtr));
 				/* No live entries on this page */
-				Assert(leafTuple->nextOffset == InvalidOffsetNumber);
+				Assert(SGLT_GET_OFFSET(leafTuple) == InvalidOffsetNumber);
 				return SpGistBreakOffsetNumber;
 			}
 		}
@@ -750,7 +774,7 @@ spgTestLeafTuple(SpGistScanOpaque so,
 
 	spgLeafTest(so, item, leafTuple, isnull, reportedSome, storeRes);
 
-	return leafTuple->nextOffset;
+	return SGLT_GET_OFFSET(leafTuple);
 }
 
 /*
@@ -782,8 +806,8 @@ redirect:
 		{
 			/* We store heap items in the queue only in case of ordered search */
 			Assert(so->numberOfNonNullOrderBys > 0);
-			storeRes(so, &item->heapPtr, item->value, item->isNull,
-					 item->recheck, item->recheckDistances, item->distances);
+			storeRes(so, &item->heapPtr, item->value, item->isNull, item->recheck,
+					 item->recheckDistances, item->distances, item->leafTuple);
 			reportedSome = true;
 		}
 		else
@@ -877,7 +901,7 @@ redirect:
 static void
 storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
 			Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			double *distances)
+			double *distances, SpGistLeafTuple leafTuple)
 {
 	Assert(!recheckDistances && !distances);
 	tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -904,7 +928,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
 static void
 storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 			  Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
-			  double *nonNullDistances)
+			  double *nonNullDistances, SpGistLeafTuple leafTuple)
 {
 	Assert(so->nPtrs < MaxIndexTuplesPerPage);
 	so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -949,9 +973,44 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
 		 * Reconstruct index data.  We have to copy the datum out of the temp
 		 * context anyway, so we may as well create the tuple here.
 		 */
-		so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
-												   &leafValue,
-												   &isnull);
+		if (so->state.tupleDescriptor->natts > 1)
+		{
+			/*
+			 * A general case of one key attribute and several INCLUDE
+			 * attributes
+			 */
+			Datum	   *leafDatums;
+			bool	   *leafIsnulls;
+
+			leafDatums = (Datum *) palloc(sizeof(Datum) * (so->state.tupleDescriptor->natts));
+			leafIsnulls = (bool *) palloc(sizeof(bool) * (so->state.tupleDescriptor->natts));
+
+			spgDeformLeafTuple(leafTuple, so->state.tupleDescriptor, leafDatums, leafIsnulls, isnull);
+
+			/*
+			 * Override key value extracted from LeafTuple in case we've
+			 * reconstructed it already
+			 */
+			leafDatums[spgKeyColumn] = leafValue;
+			leafIsnulls[spgKeyColumn] = isnull;
+
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   leafDatums,
+													   leafIsnulls);
+			pfree(leafDatums);
+			pfree(leafIsnulls);
+		}
+		else
+		{
+			/*
+			 * A particular case of no include attributes works exactly like
+			 * more general case above but don't make redundant allocations
+			 * and rewrites when there is only one attribute.
+			 */
+			so->reconTups[so->nPtrs] = heap_form_tuple(so->indexTupDesc,
+													   &leafValue,
+													   &isnull);
+		}
 	}
 	so->nPtrs++;
 }
@@ -1019,6 +1078,10 @@ spgcanreturn(Relation index, int attno)
 {
 	SpGistCache *cache;
 
+	/* INCLUDE attributes can always be fetched for index-only scans */
+	if (attno > 1)
+		return true;
+
 	/* We can do it if the opclass config function says so */
 	cache = spgGetCache(index);
 
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index d8b1815061..e0284d50eb 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -31,7 +31,11 @@
 #include "utils/index_selfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
-
+#include "access/itup.h"
+#include "access/detoast.h"
+#include "access/toast_internals.h"
+#include "access/heaptoast.h"
+#include "utils/expandeddatum.h"
 
 /*
  * SP-GiST handler function: return IndexAmRoutine with access method parameters
@@ -57,7 +61,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
 	amroutine->amcanparallel = false;
-	amroutine->amcaninclude = false;
+	amroutine->amcaninclude = true;
 	amroutine->amusemaintenanceworkmem = false;
 	amroutine->amparallelvacuumoptions =
 		VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;
@@ -105,6 +109,8 @@ SpGistCache *
 spgGetCache(Relation index)
 {
 	SpGistCache *cache;
+	int			i;
+	int			natts = IndexRelationGetNumberOfAttributes(index);
 
 	if (index->rd_amcache == NULL)
 	{
@@ -113,18 +119,42 @@ spgGetCache(Relation index)
 		FmgrInfo   *procinfo;
 		Buffer		metabuffer;
 		SpGistMetaPageData *metadata;
+		TupleDesc	tmpTupleDescriptor;
+
+		/*
+		 * SPGiST should have one key column and can also have INCLUDE columns
+		 */
+		if (IndexRelationGetNumberOfKeyAttributes(index) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("SPGiST index can have only one key column")));
+		if (natts >= INDEX_MAX_KEYS)
+			ereport(ERROR,
+					(errcode(ERRCODE_TOO_MANY_COLUMNS),
+					 errmsg("number of index columns (%d) exceeds limit (%d)",
+							natts, INDEX_MAX_KEYS)));
+
+		/* Form a tuple descriptor for all columns */
+		tmpTupleDescriptor = CreateTemplateTupleDesc(natts);
+
+		for (i = spgKeyColumn; i < natts; i++)
+			TupleDescInitEntry(tmpTupleDescriptor, i + 1, NULL,
+							   TupleDescAttr(index->rd_att, i)->atttypid, -1, 0);
 
 		cache = MemoryContextAllocZero(index->rd_indexcxt,
-									   sizeof(SpGistCache));
+									   offsetof(struct SpGistCache, tupleDescriptor) +
+									   TupleDescSize(tmpTupleDescriptor));
 
-		/* SPGiST doesn't support multi-column indexes */
-		Assert(index->rd_att->natts == 1);
+		memcpy(&cache->tupleDescriptor, tmpTupleDescriptor,
+			   TupleDescSize(tmpTupleDescriptor));
+		pfree(tmpTupleDescriptor);
 
 		/*
-		 * Get the actual data type of the indexed column from the index
-		 * tupdesc.  We pass this to the opclass config function so that
-		 * polymorphic opclasses are possible.
+		 * Get the actual data type of the key column from the index tupdesc.
+		 * We pass this to the opclass config function so that polymorphic
+		 * opclasses are possible.
 		 */
+
 		atttype = TupleDescAttr(index->rd_att, 0)->atttypid;
 
 		/* Call the config function to get config info for the opclass */
@@ -196,6 +226,7 @@ initSpGistState(SpGistState *state, Relation index)
 	state->attLeafType = cache->attLeafType;
 	state->attPrefixType = cache->attPrefixType;
 	state->attLabelType = cache->attLabelType;
+	state->tupleDescriptor = &cache->tupleDescriptor;
 
 	/* Make workspace for constructing dead tuples */
 	state->deadTupleStorage = palloc0(SGDTSIZE);
@@ -205,6 +236,7 @@ initSpGistState(SpGistState *state, Relation index)
 
 	/* Assume we're not in an index build (spgbuild will override) */
 	state->isBuild = false;
+
 }
 
 /*
@@ -604,8 +636,8 @@ spgoptions(Datum reloptions, bool validate)
 
 /*
  * Get the space needed to store a non-null datum of the indicated type.
- * Note the result is already rounded up to a MAXALIGN boundary.
- * Also, we follow the SPGiST convention that pass-by-val types are
+ * Note the result is not maxaligned and this should be done by the caller if
+ * needed. Also, we follow the SPGiST convention that pass-by-val types are
  * just stored in their Datum representation (compare memcpyDatum).
  */
 unsigned int
@@ -620,7 +652,7 @@ SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum)
 	else
 		size = VARSIZE_ANY(datum);
 
-	return MAXALIGN(size);
+	return size;
 }
 
 /*
@@ -642,36 +674,85 @@ memcpyDatum(void *target, SpGistTypeDesc *att, Datum datum)
 	}
 }
 
+Size
+spgNullmaskSize(int natts, bool *isnull)
+{
+	int			i;
+
+	Assert(natts > 0);
+	Assert(natts <= INDEX_MAX_KEYS);
+
+	/*
+	 * If there is only a key attribute (natts == 1), nullmask will not be
+	 * inserted (even inside the tuples, which have NULL key value). This
+	 * ensures compatibility with the previous versions tuple layout.
+	 */
+	if (natts == 1)
+		return 0;
+
+	for (i = spgKeyColumn; i < natts; i++)
+	{
+		/*
+		 * If there is at least one null, then nullmask will be sized to
+		 * contain a key attribute and all INCLUDE attributes.
+		 */
+		if (isnull[i])
+			return ((natts - 1) / 8) + 1;
+	}
+	return 0;
+}
+
 /*
- * Construct a leaf tuple containing the given heap TID and datum value
+ * Construct a leaf tuple containing the given heap TID, datums and isnulls arrays.
+ * Nullmask apply only to INCLUDE attribute and is placed just after header if
+ * there is at least one NULL among INCLUDE attributes. It doesn't need alignment.
+ * Then all attributes data follow starting from MAXALIGN.
  */
 SpGistLeafTuple
 spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr,
-				 Datum datum, bool isnull)
+				 Datum *datum, bool *isnull)
 {
 	SpGistLeafTuple tup;
-	unsigned int size;
-
-	/* compute space needed (note result is already maxaligned) */
-	size = SGLTHDRSZ;
-	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLeafType, datum);
+	TupleDesc	tupleDescriptor = state->tupleDescriptor;
+	uint16		tupmask = 0;
+	Size		data_size = heap_compute_data_size(tupleDescriptor, datum, isnull);
+	Size		nullmask_size = spgNullmaskSize(tupleDescriptor->natts, isnull);
+	Size		hoff = MAXALIGN(sizeof(SpGistLeafTupleData) + nullmask_size);
+	Size		size = MAXALIGN(hoff + data_size);
+	char	   *tp;				/* ptr to tuple data */
 
 	/*
-	 * Ensure that we can replace the tuple with a dead tuple later.  This
-	 * test is unnecessary when !isnull, but let's be safe.
+	 * Ensure that we can replace the tuple with a dead tuple later. This test
+	 * is unnecessary when !isnull[spgKeyColumn], but let's be safe.
 	 */
-	if (size < SGDTSIZE)
-		size = SGDTSIZE;
+	size = size < SGDTSIZE ? SGDTSIZE : MAXALIGN(size);
 
-	/* OK, form the tuple */
 	tup = (SpGistLeafTuple) palloc0(size);
-
 	tup->size = size;
-	tup->nextOffset = InvalidOffsetNumber;
+	SGLT_SET_OFFSET(tup, InvalidOffsetNumber);
 	tup->heapPtr = *heapPtr;
-	if (!isnull)
-		memcpyDatum(SGLTDATAPTR(tup), &state->attLeafType, datum);
+	tp = (char *) tup + hoff;
+
+	if (nullmask_size)
+	{
+		bits8	   *bp;			/* ptr to null bitmap in tuple */
+
+		/* Set nullmask presence bit in SpGistLeafTuple header if needed */
+		SGLT_SET_CONTAINSNULLMASK(tup, true);
+		/* Fill nullmask and data part of a tuple */
+		bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData));
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask, bp);
+	}
+	else if (tupleDescriptor->natts > 1 || isnull[0] == false)
+
+		/*
+		 * Prevent filling nullmask in the tuple in case there should be no
+		 * nullmask.
+		 */
+		heap_fill_tuple(tupleDescriptor, datum, isnull, tp, data_size, &tupmask,
+						(bits8 *) NULL);
+
+	/* Single-column tuple with NULL value doesn't need filling data portion */
 
 	return tup;
 }
@@ -689,10 +770,10 @@ spgFormNodeTuple(SpGistState *state, Datum label, bool isnull)
 	unsigned int size;
 	unsigned short infomask = 0;
 
-	/* compute space needed (note result is already maxaligned) */
+	/* compute space needed */
 	size = SGNTHDRSZ;
 	if (!isnull)
-		size += SpGistGetTypeSize(&state->attLabelType, label);
+		size += MAXALIGN(SpGistGetTypeSize(&state->attLabelType, label));
 
 	/*
 	 * Here we make sure that the size will fit in the field reserved for it
@@ -736,7 +817,7 @@ spgFormInnerTuple(SpGistState *state, bool hasPrefix, Datum prefix,
 
 	/* Compute size needed */
 	if (hasPrefix)
-		prefixSize = SpGistGetTypeSize(&state->attPrefixType, prefix);
+		prefixSize = MAXALIGN(SpGistGetTypeSize(&state->attPrefixType, prefix));
 	else
 		prefixSize = 0;
 
@@ -815,7 +896,7 @@ spgFormDeadTuple(SpGistState *state, int tupstate,
 
 	tuple->tupstate = tupstate;
 	tuple->size = SGDTSIZE;
-	tuple->nextOffset = InvalidOffsetNumber;
+	tuple->t_info = InvalidOffsetNumber;
 
 	if (tupstate == SPGIST_REDIRECT)
 	{
@@ -1047,3 +1128,49 @@ spgproperty(Oid index_oid, int attno,
 
 	return true;
 }
+
+/*
+ * Convert an SpGist tuple into palloc'd Datum/isnull arrays.
+ */
+void
+spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+				   Datum *values, bool *isnull, bool keyColumnIsNull)
+{
+	bool		hasNullsMask = SGLT_GET_CONTAINSNULLMASK(tup);
+	char	   *tp;				/* ptr to tuple data */
+	bits8	   *bp;				/* ptr to null bitmap in tuple */
+
+	if (keyColumnIsNull && tupleDescriptor->natts == 1)
+	{
+		/*
+		 * Trivial case: there is only key attribute and we're in a nulls
+		 * tree. hasNullsMask bit in a tuple header should not be set for
+		 * single attribute case even if it has NULL value (for compatibility
+		 * with pre-v14 SpGist tuple format) We should not call
+		 * index_deform_anyheader_tuple() in this trivial case as it expects
+		 * nullmask in a tuple present in this case.
+		 */
+		Assert(hasNullsMask == 0);
+
+		isnull[spgKeyColumn] = true;
+		values[spgKeyColumn] = (Datum) 0;
+		return;
+	}
+	else if (hasNullsMask)
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData) +
+									 ((tupleDescriptor->natts - 1) / 8 + 1));
+	else
+		tp = (char *) tup + MAXALIGN(sizeof(SpGistLeafTupleData));
+
+	bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData));
+
+	index_deform_anyheader_tuple((char *) tup, tupleDescriptor,
+								 values, isnull,
+								 bp, tp, hasNullsMask);
+
+	/*
+	 * Key column isnull value from a tuple should be consistent with
+	 * keyColumnIsNull got from the caller
+	 */
+	Assert(keyColumnIsNull == isnull[spgKeyColumn]);
+}
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index a9ffca5183..684efa7cd6 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -168,23 +168,28 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 			}
 
 			/* Form predecessor map, too */
-			if (lt->nextOffset != InvalidOffsetNumber)
+			if (SGLT_GET_OFFSET(lt) != InvalidOffsetNumber)
 			{
 				/* paranoia about corrupted chain links */
-				if (lt->nextOffset < FirstOffsetNumber ||
-					lt->nextOffset > max ||
-					predecessor[lt->nextOffset] != InvalidOffsetNumber)
+				if (SGLT_GET_OFFSET(lt) < FirstOffsetNumber ||
+					SGLT_GET_OFFSET(lt) > max ||
+					predecessor[SGLT_GET_OFFSET(lt)] != InvalidOffsetNumber)
 					elog(ERROR, "inconsistent tuple chain links in page %u of index \"%s\"",
 						 BufferGetBlockNumber(buffer),
 						 RelationGetRelationName(index));
-				predecessor[lt->nextOffset] = i;
+				predecessor[SGLT_GET_OFFSET(lt)] = i;
 			}
 		}
 		else if (lt->tupstate == SPGIST_REDIRECT)
 		{
 			SpGistDeadTuple dt = (SpGistDeadTuple) lt;
 
-			Assert(dt->nextOffset == InvalidOffsetNumber);
+			/*
+			 * Dead tuple nextOffset is allowed to have any values of two
+			 * highest bits in case it is inherited from SpGistLeafTuple where
+			 * these bits have their own meaning.
+			 */
+			Assert(SGLT_GET_OFFSET(dt) == InvalidOffsetNumber);
 			Assert(ItemPointerIsValid(&dt->pointer));
 
 			/*
@@ -201,7 +206,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		}
 		else
 		{
-			Assert(lt->nextOffset == InvalidOffsetNumber);
+			Assert(SGLT_GET_OFFSET(lt) == InvalidOffsetNumber);
 		}
 	}
 
@@ -250,7 +255,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		prevLive = deletable[i] ? InvalidOffsetNumber : i;
 
 		/* scan down the chain ... */
-		j = head->nextOffset;
+		j = SGLT_GET_OFFSET(head);
 		while (j != InvalidOffsetNumber)
 		{
 			SpGistLeafTuple lt;
@@ -301,7 +306,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 				interveningDeletable = false;
 			}
 
-			j = lt->nextOffset;
+			j = SGLT_GET_OFFSET(lt);
 		}
 
 		if (prevLive == InvalidOffsetNumber)
@@ -366,7 +371,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 		lt = (SpGistLeafTuple) PageGetItem(page,
 										   PageGetItemId(page, chainSrc[i]));
 		Assert(lt->tupstate == SPGIST_LIVE);
-		lt->nextOffset = chainDest[i];
+		SGLT_SET_OFFSET(lt, chainDest[i]);
 	}
 
 	MarkBufferDirty(buffer);
diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c
index d40c7b5877..a1d8686907 100644
--- a/src/backend/access/spgist/spgxlog.c
+++ b/src/backend/access/spgist/spgxlog.c
@@ -122,8 +122,8 @@ spgRedoAddLeaf(XLogReaderState *record)
 
 				head = (SpGistLeafTuple) PageGetItem(page,
 													 PageGetItemId(page, xldata->offnumHeadLeaf));
-				Assert(head->nextOffset == leafTupleHdr.nextOffset);
-				head->nextOffset = xldata->offnumLeaf;
+				Assert(SGLT_GET_OFFSET(head) == SGLT_GET_OFFSET(&leafTupleHdr));
+				SGLT_SET_OFFSET(head, xldata->offnumLeaf);
 			}
 		}
 		else
@@ -822,7 +822,7 @@ spgRedoVacuumLeaf(XLogReaderState *record)
 			lt = (SpGistLeafTuple) PageGetItem(page,
 											   PageGetItemId(page, chainSrc[i]));
 			Assert(lt->tupstate == SPGIST_LIVE);
-			lt->nextOffset = chainDest[i];
+			SGLT_SET_OFFSET(lt, chainDest[i]);
 		}
 
 		PageSetLSN(page, lsn);
diff --git a/src/include/access/itup.h b/src/include/access/itup.h
index b6813707d0..09d947a996 100644
--- a/src/include/access/itup.h
+++ b/src/include/access/itup.h
@@ -154,6 +154,9 @@ extern Datum nocache_index_getattr(IndexTuple tup, int attnum,
 								   TupleDesc tupleDesc);
 extern void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor,
 							   Datum *values, bool *isnull);
+extern void index_deform_anyheader_tuple(char *tup, TupleDesc tupleDescriptor,
+										 Datum *values, bool *isnull,
+										 bits8 *bp, char *tp, bool hasnulls);
 extern IndexTuple CopyIndexTuple(IndexTuple source);
 extern IndexTuple index_truncate_tuple(TupleDesc sourceDescriptor,
 									   IndexTuple source, int leavenatts);
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index a81bab24ea..530f8cccd8 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -22,13 +22,15 @@
 #include "utils/geo_decls.h"
 #include "utils/relcache.h"
 
-
 typedef struct SpGistOptions
 {
 	int32		varlena_header_;	/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
 } SpGistOptions;
 
+#define spgKeyColumn 0
+#define spgFirstIncludeColumn 1
+
 #define SpGistGetFillFactor(relation) \
 	(AssertMacro(relation->rd_rel->relkind == RELKIND_INDEX && \
 				 relation->rd_rel->relam == SPGIST_AM_OID), \
@@ -146,28 +148,9 @@ typedef struct SpGistState
 
 	TransactionId myXid;		/* XID to use when creating a redirect tuple */
 	bool		isBuild;		/* true if doing index build */
+	TupleDesc	tupleDescriptor;	/* tuple descriptor */
 } SpGistState;
 
-typedef struct SpGistSearchItem
-{
-	pairingheap_node phNode;	/* pairing heap node */
-	Datum		value;			/* value reconstructed from parent or
-								 * leafValue if heaptuple */
-	void	   *traversalValue; /* opclass-specific traverse value */
-	int			level;			/* level of items on this page */
-	ItemPointerData heapPtr;	/* heap info, if heap tuple */
-	bool		isNull;			/* SearchItem is NULL item */
-	bool		isLeaf;			/* SearchItem is heap item */
-	bool		recheck;		/* qual recheck is needed */
-	bool		recheckDistances;	/* distance recheck is needed */
-
-	/* array with numberOfOrderBys entries */
-	double		distances[FLEXIBLE_ARRAY_MEMBER];
-} SpGistSearchItem;
-
-#define SizeOfSpGistSearchItem(n_distances) \
-	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
-
 /*
  * Private state of an index scan
  */
@@ -243,9 +226,9 @@ typedef struct SpGistCache
 	SpGistTypeDesc attLabelType;	/* type of node label values */
 
 	SpGistLUPCache lastUsedPages;	/* local storage of last-used info */
+	TupleDescData tupleDescriptor;	/* descriptor for leaf tuples */
 } SpGistCache;
 
-
 /*
  * SPGiST tuple types.  Note: inner, leaf, and dead tuple structs
  * must have the same tupstate field in the same position!	Real inner and
@@ -305,8 +288,8 @@ typedef SpGistInnerTupleData *SpGistInnerTuple;
  * SPGiST node tuple: one node within an inner tuple
  *
  * Node tuples use the same header as ordinary Postgres IndexTuples, but
- * we do not use a null bitmap, because we know there is only one column
- * so the INDEX_NULL_MASK bit suffices.  Also, pass-by-value datums are
+ * we do not use a null bitmap, because we know there is only one key column
+ * so the INDEX_NULL_MASK bit suffices. Also, pass-by-value datums are
  * stored as a full Datum, the same convention as for inner tuple prefixes
  * and leaf tuple datums.
  */
@@ -322,23 +305,21 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
 							 PointerGetDatum(SGNTDATAPTR(x)))
 
 /*
- * SPGiST leaf tuple: carries a datum and a heap tuple TID
+ * SPGiST leaf tuple: carries a heap tuple TID and columns datums and
+ * nullmasks.
  *
- * In the simplest case, the datum is the same as the indexed value; but
+ * In the simplest case, the key datum is the same as the indexed value; but
  * it could also be a suffix or some other sort of delta that permits
  * reconstruction given knowledge of the prefix path traversed to get here.
+ * Datums of INCLUDE columns are stored without modification.
  *
  * The size field is wider than could possibly be needed for an on-disk leaf
  * tuple, but this allows us to form leaf tuples even when the datum is too
  * wide to be stored immediately, and it costs nothing because of alignment
  * considerations.
  *
- * Normally, nextOffset links to the next tuple belonging to the same parent
- * node (which must be on the same page).  But when the root page is a leaf
- * page, we don't chain its tuples, so nextOffset is always 0 on the root.
- *
  * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE
- * so that the tuple can be converted to REDIRECT status later.  (This
+ * so that the tuple can be converted to REDIRECT status later. (This
  * restriction only adds bytes for the null-datum case, otherwise alignment
  * restrictions force it anyway.)
  *
@@ -346,23 +327,65 @@ typedef SpGistNodeTupleData *SpGistNodeTuple;
  * however, the SGDTSIZE limit ensures that's there's a Datum word there
  * anyway, so SGLTDATUM can be applied safely as long as you don't do
  * anything with the result.
+ *
+ * Normally, nextOffset inside t_info links to the next tuple belonging to
+ * the same parent node (which must be on the same page).  But when the root
+ * page is a leaf page, we don't chain its tuples, so nextOffset is always 0
+ * on the root. Minimum space to store SpGistLeafTuple plus ItemIdData on a
+ * page is 16 bytes, so 15 lower bits for nextOffset is enough to store tuple
+ * number in a chain on a page even if a page size is 64Kb.
+ *
+ * The highest bit in t_info is to store per-tuple information is there nulls
+ * mask exist for the case there are INCLUDE attributes. If there are no
+ * INCLUDE columns this bit is set to 0 and nullmask is not added even if it
+ * is an empty tuple with NULL key value.
+ *
+ * Datums for all columns are stored in ordinary index-tuple-like way starting
+ * from MAXALIGN boundary. Nullmask with size (number of columns)/8
+ * bytes is put without alignment just after the ending of tuple header.
+ * On 64-bit architecture nullmask has a good chance to fit into the alignment
+ * gap between the header and the first datum, thus making its storage free
+ * of charge.
  */
+
 typedef struct SpGistLeafTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;		/* large enough for any palloc'able value */
-	OffsetNumber nextOffset;	/* next tuple in chain, or InvalidOffsetNumber */
+
+	/* ---------------
+	 * t_info is laid out in the following fashion:
+	 *
+	 * 15th (high) bit: values has nulls
+	 * 14-0 bit: nextOffset i.e. number of next tuple in chain on a page,
+	 * 			 or InvalidOffsetNumber
+	 * ---------------
+	 */
+	unsigned short t_info;		/* nextOffset for linking tuples in a chain on
+								 * a leaf page, and additional info */
 	ItemPointerData heapPtr;	/* TID of represented heap tuple */
-	/* leaf datum follows */
+	/* nullmask follows if there are nulls among attributes */
+	/* attributes data follow starting from MAXALIGN */
 } SpGistLeafTupleData;
 
 typedef SpGistLeafTupleData *SpGistLeafTuple;
 
 #define SGLTHDRSZ			MAXALIGN(sizeof(SpGistLeafTupleData))
 #define SGLTDATAPTR(x)		(((char *) (x)) + SGLTHDRSZ)
-#define SGLTDATUM(x, s)		((s)->attLeafType.attbyval ? \
-							 *(Datum *) SGLTDATAPTR(x) : \
-							 PointerGetDatum(SGLTDATAPTR(x)))
+#define SGLTDATUM(x, s)		fetch_att(SGLTDATAPTR(x), (s)->attLeafType.attbyval, \
+							(s)->attLeafType.attlen)
+/*
+ * Macros to access nextOffset and bit fields inside t_info independently.
+ */
+#define SGLT_GET_OFFSET(spgLeafTuple)	( (spgLeafTuple)->t_info & 0x3FFF )
+#define SGLT_GET_CONTAINSNULLMASK(spgLeafTuple) \
+	( (bool)((spgLeafTuple)->t_info >> 15) )
+#define SGLT_SET_OFFSET(spgLeafTuple, offsetNumber) \
+	( (spgLeafTuple)->t_info = \
+	((spgLeafTuple)->t_info & 0xC000) | ((offsetNumber) & 0x3FFF) )
+#define SGLT_SET_CONTAINSNULLMASK(spgLeafTuple, is_null) \
+	( (spgLeafTuple)->t_info = \
+	((uint16)(bool)(is_null) << 15) | ((spgLeafTuple)->t_info & 0x3FFF) )
 
 /*
  * SPGiST dead tuple: declaration for examining non-live tuples
@@ -372,14 +395,14 @@ typedef SpGistLeafTupleData *SpGistLeafTuple;
  * Also, the pointer field must be in the same place as a leaf tuple's heapPtr
  * field, to satisfy some Asserts that we make when replacing a leaf tuple
  * with a dead tuple.
- * We don't use nextOffset, but it's needed to align the pointer field.
+ * We don't use t_info, but it's needed to align the pointer field.
  * pointer and xid are only valid when tupstate = REDIRECT.
  */
 typedef struct SpGistDeadTupleData
 {
 	unsigned int tupstate:2,	/* LIVE/REDIRECT/DEAD/PLACEHOLDER */
 				size:30;
-	OffsetNumber nextOffset;	/* not used in dead tuples */
+	unsigned short t_info;		/* not used in dead tuples */
 	ItemPointerData pointer;	/* redirection inside index */
 	TransactionId xid;			/* ID of xact that inserted this tuple */
 } SpGistDeadTupleData;
@@ -394,7 +417,6 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
  * size plus sizeof(ItemIdData) (for the line pointer).  This works correctly
  * so long as tuple sizes are always maxaligned.
  */
-
 /* Page capacity after allowing for fixed header and special space */
 #define SPGIST_PAGE_CAPACITY  \
 	MAXALIGN_DOWN(BLCKSZ - \
@@ -410,6 +432,27 @@ typedef SpGistDeadTupleData *SpGistDeadTuple;
 	 Min(SpGistPageGetOpaque(p)->nPlaceholder, n) * \
 	 (SGDTSIZE + sizeof(ItemIdData)))
 
+
+typedef struct SpGistSearchItem
+{
+	pairingheap_node phNode;	/* pairing heap node */
+	Datum		value;			/* value reconstructed from parent or
+								 * leafValue if heaptuple */
+	void	   *traversalValue; /* opclass-specific traverse value */
+	int			level;			/* level of items on this page */
+	ItemPointerData heapPtr;	/* heap info, if heap tuple */
+	bool		isNull;			/* SearchItem is NULL item */
+	bool		isLeaf;			/* SearchItem is heap item */
+	bool		recheck;		/* qual recheck is needed */
+	bool		recheckDistances;	/* distance recheck is needed */
+	SpGistLeafTuple leafTuple;
+	/* array with numberOfOrderBys entries */
+	double		distances[FLEXIBLE_ARRAY_MEMBER];
+} SpGistSearchItem;
+
+#define SizeOfSpGistSearchItem(n_distances) \
+	(offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+
 /*
  * XLOG stuff
  */
@@ -456,9 +499,10 @@ extern void SpGistInitPage(Page page, uint16 f);
 extern void SpGistInitBuffer(Buffer b, uint16 f);
 extern void SpGistInitMetapage(Page page);
 extern unsigned int SpGistGetTypeSize(SpGistTypeDesc *att, Datum datum);
+extern Size spgNullmaskSize(int natts, bool *isnull);
 extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state,
 										ItemPointer heapPtr,
-										Datum datum, bool isnull);
+										Datum *datum, bool *isnull);
 extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state,
 										Datum label, bool isnull);
 extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
@@ -466,6 +510,8 @@ extern SpGistInnerTuple spgFormInnerTuple(SpGistState *state,
 										  int nNodes, SpGistNodeTuple *nodes);
 extern SpGistDeadTuple spgFormDeadTuple(SpGistState *state, int tupstate,
 										BlockNumber blkno, OffsetNumber offnum);
+extern void spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor,
+							   Datum *datum, bool *isnull, bool keyIsNull);
 extern Datum *spgExtractNodeLabels(SpGistState *state,
 								   SpGistInnerTuple innerTuple);
 extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page,
@@ -484,7 +530,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page,
 									int firststate, int reststate,
 									BlockNumber blkno, OffsetNumber offnum);
 extern bool spgdoinsert(Relation index, SpGistState *state,
-						ItemPointer heapPtr, Datum datum, bool isnull);
+						ItemPointer heapPtr, Datum *datum, bool *isnull);
 
 /* spgproc.c */
 extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
diff --git a/src/test/regress/expected/amutils.out b/src/test/regress/expected/amutils.out
index d92a6d12c6..7ab6113c61 100644
--- a/src/test/regress/expected/amutils.out
+++ b/src/test/regress/expected/amutils.out
@@ -171,7 +171,7 @@ select amname, prop, pg_indexam_has_property(a.oid, prop) as p
  spgist | can_unique    | f
  spgist | can_multi_col | f
  spgist | can_exclude   | t
- spgist | can_include   | f
+ spgist | can_include   | t
  spgist | bogus         | 
 (36 rows)
 
diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out
index 8e5d53e712..86510687c7 100644
--- a/src/test/regress/expected/index_including.out
+++ b/src/test/regress/expected/index_including.out
@@ -349,14 +349,13 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 
 DROP TABLE tbl;
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "brin" does not support included columns
 CREATE INDEX on tbl USING gist(c3) INCLUDE (c1, c4);
 CREATE INDEX on tbl USING spgist(c3) INCLUDE (c4);
-ERROR:  access method "spgist" does not support included columns
 CREATE INDEX on tbl USING gin(c1, c2) INCLUDE (c3, c4);
 ERROR:  access method "gin" does not support included columns
 CREATE INDEX on tbl USING hash(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/expected/index_including_spgist.out b/src/test/regress/expected/index_including_spgist.out
new file mode 100644
index 0000000000..213cce5c7c
--- /dev/null
+++ b/src/test/regress/expected/index_including_spgist.out
@@ -0,0 +1,143 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+                                     pg_get_indexdef                                     
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+ c1 | c2 | c3 |     c4      
+----+----+----+-------------
+  1 |  2 |  3 | (2,3),(1,2)
+  2 |  4 |  6 | (4,5),(2,3)
+  3 |  6 |  9 | (6,7),(3,4)
+  4 |  8 | 12 | (8,9),(4,5)
+(4 rows)
+
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+                     QUERY PLAN                     
+----------------------------------------------------
+ Index Only Scan using tbl_spgist_idx on tbl_spgist
+   Index Cond: (c4 <@ '(10,10),(1,1)'::box)
+(2 rows)
+
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                        indexdef                                         
+-----------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c2, c3)
+(1 row)
+
+DROP TABLE tbl_spgist;
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+                                      indexdef                                       
+-------------------------------------------------------------------------------------
+ CREATE INDEX tbl_spgist_idx ON public.tbl_spgist USING spgist (c4) INCLUDE (c1, c3)
+(1 row)
+
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ indexdef 
+----------
+(0 rows)
+
+DROP TABLE tbl_spgist;
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+             Table "public.tbl_spgist"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ c1     | bigint  |           |          | 
+ c2     | integer |           |          | 
+ c3     | bigint  |           |          | 
+ c4     | box     |           |          | 
+Indexes:
+    "tbl_spgist_idx" spgist (c4) INCLUDE (c1, c3)
+
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b3605db88c..5fa57d4201 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -52,7 +52,7 @@ test: copy copyselect copydml insert insert_conflict
 # ----------
 test: create_misc create_operator create_procedure
 # These depend on create_misc and create_operator
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist index_including_spgist
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 15ec7548e3..d89d21187b 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -68,6 +68,7 @@ test: create_index_spgist
 test: create_view
 test: index_including
 test: index_including_gist
+test: index_including_spgist
 test: create_aggregate
 test: create_function_3
 test: create_cast
diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql
index 7e517483ad..44b340053b 100644
--- a/src/test/regress/sql/index_including.sql
+++ b/src/test/regress/sql/index_including.sql
@@ -182,7 +182,7 @@ SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl' ORDER BY indexname;
 DROP TABLE tbl;
 
 /*
- * 7. Check various AMs. All but btree and gist must fail.
+ * 7. Check various AMs. All but btree, gist and spgist must fail.
  */
 CREATE TABLE tbl (c1 int,c2 int, c3 box, c4 box);
 CREATE INDEX on tbl USING brin(c1, c2) INCLUDE (c3, c4);
diff --git a/src/test/regress/sql/index_including_spgist.sql b/src/test/regress/sql/index_including_spgist.sql
new file mode 100644
index 0000000000..38ace74d4e
--- /dev/null
+++ b/src/test/regress/sql/index_including_spgist.sql
@@ -0,0 +1,84 @@
+/*
+ * 1.1. test CREATE INDEX with buffered build
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+SET enable_seqscan TO off;
+-- size is chosen to exceed page size and trigger actual truncation
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 1.2. test CREATE INDEX with inserts
+ */
+
+-- Regular index with included columns
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+-- size is chosen to exceed page size and trigger actual truncation
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,8000) AS x;
+SELECT pg_get_indexdef(i.indexrelid)
+FROM pg_index i JOIN pg_class c ON i.indexrelid = c.oid
+WHERE i.indrelid = 'tbl_spgist'::regclass ORDER BY c.relname;
+SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO off;
+VACUUM ANALYZE tbl_spgist;
+EXPLAIN  (costs off) SELECT * FROM tbl_spgist where c4 <@ box(point(1,1),point(10,10));
+SET enable_bitmapscan TO default;
+DROP TABLE tbl_spgist;
+
+/*
+ * 2. CREATE INDEX CONCURRENTLY
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX CONCURRENTLY tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c2,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+
+/*
+ * 3. REINDEX
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+REINDEX INDEX tbl_spgist_idx;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+ALTER TABLE tbl_spgist DROP COLUMN c1;
+SELECT indexdef FROM pg_indexes WHERE tablename = 'tbl_spgist' ORDER BY indexname;
+DROP TABLE tbl_spgist;
+
+/*
+ * 4. Update, delete values in indexed table.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+UPDATE tbl_spgist SET c1 = 100 WHERE c1 = 2;
+UPDATE tbl_spgist SET c1 = 1 WHERE c1 = 3;
+DELETE FROM tbl_spgist WHERE c1 = 5 OR c3 = 12;
+DROP TABLE tbl_spgist;
+
+/*
+ * 5. Alter column type.
+ */
+CREATE TABLE tbl_spgist (c1 int, c2 int, c3 int, c4 box);
+INSERT INTO tbl_spgist SELECT x, 2*x, 3*x, box(point(x,x+1),point(2*x,2*x+1)) FROM generate_series(1,10) AS x;
+CREATE INDEX tbl_spgist_idx ON tbl_spgist using spgist (c4) INCLUDE (c1,c3);
+ALTER TABLE tbl_spgist ALTER c1 TYPE bigint;
+ALTER TABLE tbl_spgist ALTER c3 TYPE bigint;
+\d tbl_spgist
+RESET enable_seqscan;
+DROP TABLE tbl_spgist;
-- 
2.28.0

#30Tom Lane
tgl@sss.pgh.pa.us
In reply to: Pavel Borisov (#29)
Re: [PATCH] Covering SPGiST index

Pavel Borisov <pashkin.elfe@gmail.com> writes:

In a v14 I forgot to add the test. PFA v15

I've committed this with a lot of mostly-cosmetic changes.
The not-so-cosmetic bits had to do with confusion between
the input data type and the leaf type, which isn't really
your fault because it was there before :-(.

One note is that I dropped the added regression test script
(index_including_spgist.sql) entirely, because I couldn't
see that it did anything that justified a permanent expenditure
of test cycles. It looks like you made that by doing s/gist/spgist/g
on index_including_gist.sql, which might be all right except that
that script was designed to test GiST-specific implementation concerns
that aren't too relevant to SP-GiST. AFAICT, removing that script had
exactly zero effect on the test coverage shown by gcov. There are
certainly bits of spgist that are depressingly under-covered, but I'm
afraid we need custom-designed test cases to get at them.

(wanders away wondering if the isolationtester could be used to test
the concurrency-sensitive parts of spgvacuum.c ...)

regards, tom lane

#31Pavel Borisov
pashkin.elfe@gmail.com
In reply to: Tom Lane (#30)
2 attachment(s)
Re: [PATCH] Covering SPGiST index

I've committed this with a lot of mostly-cosmetic changes.
The not-so-cosmetic bits had to do with confusion between
the input data type and the leaf type, which isn't really
your fault because it was there before :-(.

One note is that I dropped the added regression test script
(index_including_spgist.sql) entirely, because I couldn't
see that it did anything that justified a permanent expenditure
of test cycles. It looks like you made that by doing s/gist/spgist/g
on index_including_gist.sql, which might be all right except that
that script was designed to test GiST-specific implementation concerns
that aren't too relevant to SP-GiST. AFAICT, removing that script had
exactly zero effect on the test coverage shown by gcov. There are
certainly bits of spgist that are depressingly under-covered, but I'm
afraid we need custom-designed test cases to get at them.

(wanders away wondering if the isolationtester could be used to test
the concurrency-sensitive parts of spgvacuum.c ...)

regards, tom lane

Thanks a lot!
As for tests I mostly checked the storage and reconstruction of included
attributes in the spgist index with radix and quadtree, with many included
columns of different types and nulls among the values. But I consider it
too big for regression. I attach radix test just in case. Do you consider
something like this could be useful for testing and should I try to adapt
something like this to make regression? Or do something like this on some
database already in the regression suite?

--
Best regards,
Pavel Borisov

Postgres Professional: http://postgrespro.com <http://www.postgrespro.com&gt;

Attachments:

urls-short.txttext/plain; charset=US-ASCII; name=urls-short.txtDownload
spgist-radix-test.sqlapplication/octet-stream; name=spgist-radix-test.sqlDownload