[Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Started by Aleksander Alekseevover 9 years ago63 messages
#1Aleksander Alekseev
a.alekseev@postgrespro.ru
1 attachment(s)

Hello

Some time ago we discussed an idea of "fast temporary tables":

/messages/by-id/20160301182500.2c81c3dc@fujitsu

In two words the idea is following.

<The Idea>

PostgreSQL stores information about all relations in pg_catalog. Some
applications create and delete a lot of temporary tables. It causes a
bloating of pg_catalog and running auto vacuum on it. It's quite an
expensive operation which affects entire database performance.

We could introduce a new type of temporary tables. Information about
these tables is stored not in a catalog but in backend's memory. This
way user can solve a pg_catalog bloating problem and improve overall
database performance.

</The Idea>

I took me a few months but eventually I made it work. Attached patch
has some flaws. I decided not to invest a lot of time in documenting
it or pgindent'ing all files yet. In my experience it will be rewritten
entirely 3 or 4 times before merging anyway :) But it _works_ and
passes all tests I could think of, including non-trivial cases like
index-only or bitmap scans of catalog tables.

Usage example:

```
CREATE FAST TEMP TABLE fasttab_test1(x int, s text);

INSERT INTO fasttab_test1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc');

UPDATE fasttab_test1 SET s = 'ddd' WHERE x = 2;

DELETE FROM fasttab_test1 WHERE x = 3;

SELECT * FROM fasttab_test1 ORDER BY x;

DROP TABLE fasttab_test1;
```

More sophisticated examples could be find in regression tests:

./src/test/regress/sql/fast_temp.sql

Any feedback on this patch will be much appreciated!

--
Best regards,
Aleksander Alekseev

Attachments:

fast-temporary-tables-v1.patchtext/x-patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 0689cc9..940210b 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1693,7 +1693,7 @@
       <entry></entry>
       <entry>
        <literal>p</> = permanent table, <literal>u</> = unlogged table,
-       <literal>t</> = temporary table
+       <literal>t</> = temporary table, <literal>f</> = fast temporary table
       </entry>
      </row>
 
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index 1fa6de0..56de4dc 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -12,7 +12,7 @@ subdir = src/backend/access/common
 top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
-OBJS = heaptuple.o indextuple.o printtup.o reloptions.o scankey.o \
+OBJS = fasttab.o heaptuple.o indextuple.o printtup.o reloptions.o scankey.o \
 	tupconvert.o tupdesc.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/common/fasttab.c b/src/backend/access/common/fasttab.c
new file mode 100644
index 0000000..0a20247
--- /dev/null
+++ b/src/backend/access/common/fasttab.c
@@ -0,0 +1,1678 @@
+/* TODO TODO comment the general idea - in-memory tuples and indexes, hooks principle, FasttabSnapshots, etc */
+
+#include "c.h"
+#include "postgres.h"
+#include "pgstat.h"
+#include "miscadmin.h"
+#include "access/amapi.h"
+#include "access/fasttab.h"
+#include "access/relscan.h"
+#include "access/valid.h"
+#include "access/sysattr.h"
+#include "access/htup_details.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_depend.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_statistic.h"
+#include "storage/bufmgr.h"
+#include "utils/rel.h"
+#include "utils/inval.h"
+#include "utils/memutils.h"
+
+/*****************************************************************************
+		  TYPEDEFS, MACRO DECLARATIONS AND CONST STATIC VARIABLES
+ *****************************************************************************/
+
+/* #define FASTTAB_DEBUG 1 */
+
+#ifdef FASTTAB_DEBUG
+static int32 fasttab_scan_tuples_counter = -1;
+#endif
+
+/* list of in-memory catalog tuples */
+typedef struct
+{
+	dlist_node	node;
+	HeapTuple	tup;
+}	DListHeapTupleData;
+
+typedef DListHeapTupleData *DListHeapTuple;
+
+#define FasttabSnapshotIsAnonymous(sn) ( !PointerIsValid((sn)->name) )
+#define FasttabSnapshotIsRoot(sn) ( !PointerIsValid((sn)->prev) )
+#define FasttabTransactionInProgress() \
+	( PointerIsValid(FasttabSnapshotGetCurrent()->prev))
+/* like strcmp but for integer types --- int, uint32, Oid, etc */
+#define FasttabCompareInts(x, y) ( (x) == (y) ? 0 : ( (x) > (y) ? 1 : -1 ))
+
+struct FasttabSnapshotData;		/* forward declaration required for
+								 * relation_is_inmem_tuple_function typedef */
+typedef struct FasttabSnapshotData *FasttabSnapshot;
+
+typedef bool (*relation_is_inmem_tuple_function)
+			(Relation relation, HeapTuple tup, FasttabSnapshot fasttab_snapshot,
+						 int tableIdx);
+
+#define FasttabRelationMaxOidAttributes 2
+
+typedef const struct
+{
+	Oid			relationId;
+	relation_is_inmem_tuple_function is_inmem_tuple_fn;
+	AttrNumber	noidattr;
+	AttrNumber	attrNumbers[FasttabRelationMaxOidAttributes];
+}	FasttabRelationMethodsData;
+
+typedef FasttabRelationMethodsData const *FasttabRelationMethods;
+
+static bool generic_is_inmem_tuple(Relation relation, HeapTuple tup,
+					   FasttabSnapshot fasttab_snapshot, int tableIdx);
+static bool pg_class_is_inmem_tuple(Relation relation, HeapTuple tup,
+						FasttabSnapshot fasttab_snapshot, int tableIdx);
+
+/* NB: keep this array sorted by relationId */
+static FasttabRelationMethodsData FasttabRelationMethodsTable[] =
+{
+	/* 1247 */
+	{TypeRelationId, &generic_is_inmem_tuple, 1,
+		{Anum_pg_type_typrelid, 0}
+	},
+	/* 1249 */
+	{AttributeRelationId, &generic_is_inmem_tuple, 1,
+		{Anum_pg_attribute_attrelid, 0}
+	},
+	/* 1259 */
+	{RelationRelationId, &pg_class_is_inmem_tuple, 0,
+		{0, 0}
+	},
+	/* 2608 */
+	{DependRelationId, &generic_is_inmem_tuple, 2,
+		{Anum_pg_depend_objid, Anum_pg_depend_refobjid}
+	},
+	/* 2611 */
+	{InheritsRelationId, &generic_is_inmem_tuple, 2,
+		{Anum_pg_inherits_inhrelid, Anum_pg_inherits_inhparent}
+	},
+	/* 2619 */
+	{StatisticRelationId, &generic_is_inmem_tuple, 1,
+		{Anum_pg_statistic_starelid, 0}
+	},
+};
+
+#define FasttabIndexMaxAttributes 3
+
+typedef enum FasttabCompareMethod
+{
+	CompareInvalid,
+	CompareOid,
+	CompareCString,
+	CompareInt16,
+	CompareInt64,
+	CompareBoolean,
+}	FasttabCompareMethod;
+
+/* typedef is in fasttab.h */
+struct FasttabIndexMethodsData
+{
+	Oid			indexId;
+	AttrNumber	nattr;
+	AttrNumber	attrNumbers[FasttabIndexMaxAttributes]; /* NB: can be negative */
+	FasttabCompareMethod attrCompareMethod[FasttabIndexMaxAttributes];
+};
+
+/*
+ * NB: Keep this array sorted by indexId!
+ * NB: Uniqueness checks are not implemented and apparently are not required.
+ * Still please keep comments regarding uniqueness, just in case.
+ */
+static FasttabIndexMethodsData FasttabIndexMethodsTable[] =
+{
+	/* 2187, non-unique */
+	{InheritsParentIndexId, 1,
+		{Anum_pg_inherits_inhparent, 0, 0},
+		{CompareOid, CompareInvalid, CompareInvalid}
+	},
+	/* 2658, unique */
+	{AttributeRelidNameIndexId, 2,
+		{Anum_pg_attribute_attrelid, Anum_pg_attribute_attname, 0},
+		{CompareOid, CompareCString, CompareInvalid}
+	},
+	/* 2659, unique */
+	{AttributeRelidNumIndexId, 2,
+		{Anum_pg_attribute_attrelid, Anum_pg_attribute_attnum, 0},
+		{CompareOid, CompareInt16, CompareInvalid}
+	},
+	/* 2662, unique */
+	{ClassOidIndexId, 1,
+		{ObjectIdAttributeNumber, 0, 0},
+		{CompareOid, CompareInvalid, CompareInvalid}
+	},
+	/* 2663, unique */
+	{ClassNameNspIndexId, 2,
+		{Anum_pg_class_relname, Anum_pg_class_relnamespace, 0},
+		{CompareCString, CompareOid, CompareInvalid}
+	},
+	/* 2673, non-unique */
+	{DependDependerIndexId, 3,
+		{Anum_pg_depend_classid, Anum_pg_depend_objid, Anum_pg_depend_objsubid},
+		{CompareOid, CompareOid, CompareInt64}
+	},
+	/* 2674, non-unique */
+	{DependReferenceIndexId, 3,
+		{Anum_pg_depend_refclassid, Anum_pg_depend_refobjid,
+		Anum_pg_depend_refobjsubid},
+		{CompareOid, CompareOid, CompareInt64}
+	},
+	/* 2680, unique */
+	{InheritsRelidSeqnoIndexId, 2,
+		{Anum_pg_inherits_inhrelid, Anum_pg_inherits_inhseqno, 0},
+		{CompareOid, CompareOid, CompareInvalid}
+	},
+	/* 2696, unique */
+	{StatisticRelidAttnumInhIndexId, 3,
+		{Anum_pg_statistic_starelid, Anum_pg_statistic_staattnum,
+		Anum_pg_statistic_stainherit},
+		{CompareOid, CompareInt16, CompareBoolean}
+	},
+	/* 2703, unique */
+	{TypeOidIndexId, 1,
+		{ObjectIdAttributeNumber, 0, 0},
+		{CompareOid, CompareInvalid, CompareInvalid}
+	},
+	/* 2704, unique */
+	{TypeNameNspIndexId, 2,
+		{Anum_pg_type_typname, Anum_pg_type_typnamespace, 0},
+		{CompareCString, CompareOid, CompareInvalid}
+	},
+	/* 3455, non-unique */
+	{ClassTblspcRelfilenodeIndexId, 2,
+		{Anum_pg_class_reltablespace, Anum_pg_class_relfilenode, 0},
+		{CompareOid, CompareOid, CompareInvalid}
+	},
+};
+
+#define FasttabSnapshotTablesNumber (lengthof(FasttabRelationMethodsTable))
+
+typedef struct
+{
+	int			tuples_num;
+	dlist_head	tuples;
+}	FasttabSnapshotRelationData;
+
+/* snapshot (i.e. transaction/savepoint) representation */
+struct FasttabSnapshotData
+{
+	struct FasttabSnapshotData *prev;
+	char	   *name;			/* NULL for root and anonymous snapshots */
+
+	/* NB: see GetSnapshotRelationIdxByOid */
+	FasttabSnapshotRelationData relationData[FasttabSnapshotTablesNumber];
+}	FasttabSnapshotData;
+
+/*****************************************************************************
+							 GLOBAL VARIABLES
+ *****************************************************************************/
+
+/* for GetLocalMemoryContext */
+static MemoryContext LocalMemoryContextPrivate = NULL;
+
+/* for GenFasttabItemPointerData */
+static uint32 CurrentFasttabBlockId = 0;
+static uint16 CurrentFasttabOffset = 1; /* 0 is considered invalid */
+
+/* for FasttabSnapshotGetCurrent */
+static FasttabSnapshot CurrentFasttabSnapshotPrivate = NULL;
+
+static char CurrentRelpersistenceHint = RELPERSISTENCE_UNDEFINED;
+
+/*****************************************************************************
+							UTILITY PROCEDURES
+ *****************************************************************************/
+
+void
+fasttab_set_relpersistence_hint(char relpersistence)
+{
+	CurrentRelpersistenceHint = relpersistence;
+}
+
+void
+fasttab_clear_relpersistence_hint(void)
+{
+	CurrentRelpersistenceHint = RELPERSISTENCE_UNDEFINED;
+}
+
+/*
+ * binary search in FasttabRelationMethodsTable
+ * == -1 - not found
+ * > 0 - found on N-th position
+ */
+static int
+GetSnapshotRelationIdxByOidInternal(Oid relId)
+{
+	int			begin = 0;
+	int			end = FasttabSnapshotTablesNumber - 1;
+
+#ifdef USE_ASSERT_CHECKING
+	/* test that FasttabRelationMethodsTable is properly sorted */
+	int			i;
+
+	for (i = 0; i <= end; i++)
+	{
+		Assert(PointerIsValid(FasttabRelationMethodsTable[i].is_inmem_tuple_fn));
+		if (i > 0)
+			Assert(FasttabRelationMethodsTable[i - 1].relationId < FasttabRelationMethodsTable[i].relationId);
+	}
+#endif
+
+	while (begin < end)
+	{
+		int			test = (begin + end) / 2;
+
+		if (FasttabRelationMethodsTable[test].relationId == relId)
+		{
+			begin = test;
+			break;
+		}
+
+		if (FasttabRelationMethodsTable[test].relationId < relId)
+			begin = test + 1;	/* go right */
+		else
+			end = test - 1;		/* go left */
+	}
+
+	if (FasttabRelationMethodsTable[begin].relationId == relId)
+		return begin;			/* found */
+	else
+		return -1;				/* not found */
+}
+
+bool
+IsFasttabHandledRelationId(Oid relId)
+{
+	return (GetSnapshotRelationIdxByOidInternal(relId) >= 0);
+}
+
+static int
+GetSnapshotRelationIdxByOid(Oid relId)
+{
+	int			result;
+
+	Assert(IsFasttabHandledRelationId(relId));
+	result = GetSnapshotRelationIdxByOidInternal(relId);
+	Assert(result >= 0 && result < FasttabSnapshotTablesNumber);
+	return result;
+}
+
+/*
+ * binary search in FasttabIndexMethodsTable
+ * == NULL - not found
+ * != NULL - found
+ */
+static FasttabIndexMethods
+GetFasttabIndexMethodsInternal(Oid indexId)
+{
+	int			begin = 0;
+	int			end = (sizeof(FasttabIndexMethodsTable) /
+					   sizeof(FasttabIndexMethodsTable[0]) - 1);
+
+#ifdef USE_ASSERT_CHECKING
+	/* test that FasttabIndexMethodsTable is properly sorted */
+	int			i;
+
+	for (i = 0; i <= end; i++)
+	{
+		if (i > 0)
+			Assert(FasttabIndexMethodsTable[i - 1].indexId < FasttabIndexMethodsTable[i].indexId);
+	}
+#endif
+
+	while (begin < end)
+	{
+		int			test = (begin + end) / 2;
+
+		if (FasttabIndexMethodsTable[test].indexId == indexId)
+		{
+			begin = test;
+			break;
+		}
+
+		if (FasttabIndexMethodsTable[test].indexId < indexId)
+			begin = test + 1;	/* go right */
+		else
+			end = test - 1;		/* go left */
+	}
+
+	if (FasttabIndexMethodsTable[begin].indexId == indexId)
+		return &FasttabIndexMethodsTable[begin];		/* found */
+	else
+		return NULL;			/* not found */
+}
+
+bool
+IsFasttabHandledIndexId(Oid indexId)
+{
+	return (GetFasttabIndexMethodsInternal(indexId) != NULL);
+}
+
+/*
+ * same as GetFasttabIndexMethodsInternal
+ * but never returns NULL
+ */
+inline static FasttabIndexMethods
+GetFasttabIndexMethods(Oid indexId)
+{
+	Assert(IsFasttabHandledIndexId(indexId));
+	return GetFasttabIndexMethodsInternal(indexId);
+}
+
+static MemoryContext
+GetLocalMemoryContext(void)
+{
+	if (!PointerIsValid(LocalMemoryContextPrivate))
+	{
+		LocalMemoryContextPrivate = AllocSetContextCreate(
+														  NULL,
+									  "Fast temporary tables memory context",
+													ALLOCSET_DEFAULT_MINSIZE,
+												   ALLOCSET_DEFAULT_INITSIZE,
+												   ALLOCSET_DEFAULT_MAXSIZE);
+	}
+
+	return LocalMemoryContextPrivate;
+}
+
+static ItemPointerData
+GenFasttabItemPointerData(void)
+{
+	ItemPointerData res;
+
+	BlockIdSet(&(res.ip_blkid), CurrentFasttabBlockId);
+	res.ip_posid = CurrentFasttabOffset | FASTTAB_ITEM_POINTER_BIT;
+
+	CurrentFasttabOffset++;
+
+	if (CurrentFasttabOffset > MaxHeapTuplesPerPage)
+	{
+		CurrentFasttabOffset = 1;
+		CurrentFasttabBlockId++;
+
+#ifdef FASTTAB_DEBUG
+		elog(NOTICE, "FASTTAB: GenFasttabItemPointerData, CurrentFasttabOffset > MaxHeapTuplesPerPage (%d), new values - CurrentFasttabOffset = %d, CurrentFasttabBlockId = %d",
+		  MaxHeapTuplesPerPage, CurrentFasttabOffset, CurrentFasttabBlockId);
+#endif
+	}
+
+	return res;
+}
+
+static FasttabSnapshot
+FasttabSnapshotCreateEmpty(void)
+{
+	FasttabSnapshot result;
+	MemoryContext oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+
+	result = palloc0(sizeof(FasttabSnapshotData));
+	MemoryContextSwitchTo(oldctx);
+	return result;
+}
+
+static FasttabSnapshot
+FasttabSnapshotCopy(FasttabSnapshot src, const char *dst_name)
+{
+	int			idx;
+	dlist_iter	iter;
+	MemoryContext oldctx;
+	FasttabSnapshot dst = FasttabSnapshotCreateEmpty();
+
+	oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+	dst->name = dst_name ? pstrdup(dst_name) : NULL;
+
+	for (idx = 0; idx < FasttabSnapshotTablesNumber; idx++)
+	{
+		dst->relationData[idx].tuples_num = src->relationData[idx].tuples_num;
+		dlist_foreach(iter, &src->relationData[idx].tuples)
+		{
+			DListHeapTuple src_dlist_tup = (DListHeapTuple) iter.cur;
+			DListHeapTuple dst_dlist_tup = palloc0(sizeof(DListHeapTupleData));
+
+			dst_dlist_tup->tup = heap_copytuple(src_dlist_tup->tup);
+			dlist_push_tail(&dst->relationData[idx].tuples,
+							&dst_dlist_tup->node);
+		}
+	}
+
+	MemoryContextSwitchTo(oldctx);
+	return dst;
+}
+
+static void
+DListHeapTupleFree(DListHeapTuple dlist_tup)
+{
+	heap_freetuple(dlist_tup->tup);
+	pfree(dlist_tup);
+}
+
+static void
+FasttabDListFree(dlist_head *head)
+{
+	while (!dlist_is_empty(head))
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) dlist_pop_head_node(head);
+
+		DListHeapTupleFree(dlist_tup);
+	}
+}
+
+static void
+FasttabSnapshotFree(FasttabSnapshot fasttab_snapshot)
+{
+	int			idx;
+
+	for (idx = 0; idx < FasttabSnapshotTablesNumber; idx++)
+		FasttabDListFree(&fasttab_snapshot->relationData[idx].tuples);
+
+	if (PointerIsValid(fasttab_snapshot->name))
+		pfree(fasttab_snapshot->name);
+
+	pfree(fasttab_snapshot);
+}
+
+static FasttabSnapshot
+FasttabSnapshotGetCurrent(void)
+{
+	if (!PointerIsValid(CurrentFasttabSnapshotPrivate))
+		CurrentFasttabSnapshotPrivate = FasttabSnapshotCreateEmpty();
+
+	return CurrentFasttabSnapshotPrivate;
+}
+
+static void
+FasttabSnapshotPushFront(FasttabSnapshot fasttab_snapshot)
+{
+	FasttabSnapshot temp = FasttabSnapshotGetCurrent();
+
+	while (!FasttabSnapshotIsRoot(temp))
+		temp = temp->prev;
+
+	temp->prev = fasttab_snapshot;
+	fasttab_snapshot->prev = NULL;
+}
+
+static inline void
+FasttabSnapshotPushBack(FasttabSnapshot fasttab_snapshot)
+{
+	fasttab_snapshot->prev = FasttabSnapshotGetCurrent();
+	CurrentFasttabSnapshotPrivate = fasttab_snapshot;
+}
+
+/* valid FasttabSnapshot or NULL if only root snapshot is left */
+static FasttabSnapshot
+FasttabSnapshotPopBack(void)
+{
+	FasttabSnapshot curr = FasttabSnapshotGetCurrent();
+
+	if (FasttabSnapshotIsRoot(curr))
+		return NULL;
+
+	CurrentFasttabSnapshotPrivate = curr->prev;
+	curr->prev = NULL;
+	return curr;
+}
+
+static void
+FasttabSnapshotCreate(const char *name)
+{
+	FasttabSnapshot src = FasttabSnapshotGetCurrent();
+	FasttabSnapshot dst = FasttabSnapshotCopy(src, name);
+
+	FasttabSnapshotPushBack(dst);
+}
+
+/*****************************************************************************
+							 HOOKS IMPLEMENTATION
+ *****************************************************************************/
+
+/* on BEGIN (there could be already a transaction in progress!) */
+void
+fasttab_begin_transaction(void)
+{
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_begin_transaction, transaction is already in progress: %u",
+		 FasttabTransactionInProgress());
+#endif
+
+	if (FasttabTransactionInProgress())
+		return;
+
+	/* begin transaction */
+	FasttabSnapshotCreate(NULL);
+	Assert(FasttabTransactionInProgress());
+	Assert(FasttabSnapshotIsAnonymous(FasttabSnapshotGetCurrent()));
+}
+
+/* on COMMIT (there could be no transaction in progress!) */
+void
+fasttab_end_transaction(void)
+{
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_end_transaction result = %u (1 - commit, 0 - rollback)"
+		 ", transaction is in progress: %u", result, FasttabTransactionInProgress());
+#endif
+
+	if (!FasttabTransactionInProgress())
+		return;
+
+	Assert(FasttabSnapshotIsAnonymous(FasttabSnapshotGetCurrent()));
+
+	/*
+	 * commit transaction - 1) save top snapshot to the bottom of snapshots
+	 * stack 2) get rid of all snapshots except the root
+	 */
+	FasttabSnapshotPushFront(FasttabSnapshotPopBack());
+	fasttab_abort_transaction();
+}
+
+/* on ROLLBACK (maybe there is in fact no transaction running!) */
+void
+fasttab_abort_transaction(void)
+{
+	FasttabSnapshot fasttab_snapshot;
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_abort_transaction, transaction is in progress: %u (it's OK if this procedure is called from fasttab_end_transaction - see the code)",
+		 FasttabTransactionInProgress());
+#endif
+
+	if (!FasttabTransactionInProgress())
+		return;
+
+	for (;;)
+	{
+		fasttab_snapshot = FasttabSnapshotPopBack();
+		if (!fasttab_snapshot)	/* nothing left to pop */
+			break;
+
+		FasttabSnapshotFree(fasttab_snapshot);
+	}
+
+	Assert(!FasttabTransactionInProgress());
+}
+
+/* on SAVEPOINT name; */
+void
+fasttab_define_savepoint(const char *name)
+{
+	Assert(FasttabTransactionInProgress());
+	Assert(FasttabSnapshotIsAnonymous(FasttabSnapshotGetCurrent()));
+
+	/*
+	 * name could be NULL in 'rollback to savepoint' case this case is already
+	 * handled by fasttab_rollback_to_savepoint
+	 */
+	if (!PointerIsValid(name))
+		return;
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_define_safepoint, name = '%s'", name);
+#endif
+
+	FasttabSnapshotCreate(name);	/* savepoint to rollback to */
+	FasttabSnapshotCreate(NULL);	/* current snapshot to store changes */
+
+	Assert(FasttabTransactionInProgress());
+}
+
+/*
+ * on ROLLBACK TO SAVEPOINT name;
+ * NB: there is no need to re-check that this savepoint exists.
+ * case of name (upper/lower) is valid too, no need to re-check this
+ */
+void
+fasttab_rollback_to_savepoint(const char *name)
+{
+	Assert(PointerIsValid(name));
+	Assert(FasttabTransactionInProgress());
+	Assert(FasttabSnapshotIsAnonymous(FasttabSnapshotGetCurrent()));
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_rollback_to_savepoint, name = '%s'", name);
+#endif
+
+	for (;;)
+	{
+		FasttabSnapshot fasttab_snapshot = FasttabSnapshotGetCurrent();
+
+		Assert(!FasttabSnapshotIsRoot(fasttab_snapshot));
+
+		if ((!FasttabSnapshotIsAnonymous(fasttab_snapshot)) &&
+			(strcmp(fasttab_snapshot->name, name) == 0))
+			break;
+
+		FasttabSnapshotFree(FasttabSnapshotPopBack());
+	}
+
+	/* create a new current snapshot to store changes */
+	FasttabSnapshotCreate(NULL);
+}
+
+/* before returning from heap_beginscan_internal or heap_rescan */
+void
+fasttab_beginscan(HeapScanDesc scan)
+{
+	int			idx;
+	Oid			relid = RelationGetRelid(scan->rs_rd);
+	FasttabSnapshot fasttab_snapshot;
+
+	if (!IsFasttabHandledRelationId(relid))
+		return;
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+
+	idx = GetSnapshotRelationIdxByOid(relid);
+	if (dlist_is_empty(&fasttab_snapshot->relationData[idx].tuples))
+		scan->rs_curr_inmem_tupnode = NULL;
+	else
+		scan->rs_curr_inmem_tupnode = dlist_head_node(&fasttab_snapshot->relationData[idx].tuples);
+
+#ifdef FASTTAB_DEBUG
+	fasttab_scan_tuples_counter = 0;
+	elog(NOTICE, "FASTTAB: fasttab_beginscan, returning scan = %p, rs_curr_inmem_tupnode = %p", scan, scan->rs_curr_inmem_tupnode);
+#endif
+}
+
+/*
+ * in heap_getnext
+ * returns valid heap tuple if return value should be replaced or NULL otherwise
+ */
+HeapTuple
+fasttab_getnext(HeapScanDesc scan, ScanDirection direction)
+{
+	bool		match;
+	int			idx;
+	FasttabSnapshot fasttab_snapshot;
+	DListHeapTuple dlist_tup;
+	dlist_node *ret_node;
+
+	if (!IsFasttabHandledRelationId(RelationGetRelid(scan->rs_rd)))
+		return NULL;
+
+	/* other directions are not used */
+	Assert(ScanDirectionIsForward(direction));
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(scan->rs_rd));
+
+	/*
+	 * simple strategy - first return all in-memory tuples, then proceed with
+	 * others
+	 */
+	while (scan->rs_curr_inmem_tupnode) /* inmemory tuples enumiration is
+										 * still in progress? */
+	{
+		ret_node = scan->rs_curr_inmem_tupnode;
+
+		if (dlist_has_next(&fasttab_snapshot->relationData[idx].tuples, ret_node))
+			scan->rs_curr_inmem_tupnode = dlist_next_node(&fasttab_snapshot->relationData[idx].tuples, ret_node);
+		else
+			scan->rs_curr_inmem_tupnode = NULL;
+
+		dlist_tup = (DListHeapTuple) ret_node;
+
+#ifdef FASTTAB_DEBUG
+		fasttab_scan_tuples_counter++;
+		elog(NOTICE, "FASTTAB: fasttab_getnext, scan = %p, counter = %u, direction = %d, return tuple t_self = %08X/%04X, oid = %d",
+			 scan, fasttab_scan_tuples_counter, direction,
+			 BlockIdGetBlockNumber(&dlist_tup->tup->t_self.ip_blkid), dlist_tup->tup->t_self.ip_posid, HeapTupleGetOid(dlist_tup->tup)
+			);
+#endif
+
+		/* HeapKeyTest is a macro, it changes `match` variable */
+		HeapKeyTest(dlist_tup->tup, RelationGetDescr(scan->rs_rd), scan->rs_nkeys, scan->rs_key, match);
+		if (!match)
+			continue;
+
+		return dlist_tup->tup;
+	}
+
+	return NULL;
+}
+
+/*
+ * Used in heap_hot_search_buffer
+ * true on override result, false otherwise
+ */
+bool
+fasttab_hot_search_buffer(ItemPointer tid, Relation relation,
+						  HeapTuple heapTuple, bool *all_dead, bool *result)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+	bool		found = false;
+
+	if (!IsFasttabItemPointer(tid))
+		return false;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(relation));
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+
+		if (ItemPointerEquals(&dlist_tup->tup->t_self, tid))
+		{
+			memcpy(heapTuple, dlist_tup->tup, sizeof(HeapTupleData));
+			found = true;
+			break;
+		}
+	}
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_hot_search_buffer, tid = %08X/%04X, found = %u",
+		 BlockIdGetBlockNumber(&tid->ip_blkid), tid->ip_posid, found);
+#endif
+
+	/* `all_dead` can be NULL during bitmap scan */
+	if (all_dead)
+		*all_dead = false;
+	/* apparently `result` can be false in ALTER TABLE case */
+	*result = found;
+	return true;
+}
+
+/*
+ * in heap_insert
+ * true on override, false otherwise
+ */
+bool
+fasttab_insert(Relation relation, HeapTuple tup, HeapTuple heaptup, Oid *result)
+{
+	FasttabSnapshot fasttab_snapshot;
+	MemoryContext oldctx;
+	DListHeapTuple dlist_tup;
+	int			idx = GetSnapshotRelationIdxByOidInternal(RelationGetRelid(relation));
+
+	if (idx < 0)				/* i.e. `!IsFasttabHandledRelationId` */
+		return false;
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+
+	/*
+	 * NB: passing idx is kind of optimization, it could be actually
+	 * re-calculated from relation argument
+	 */
+	if (!FasttabRelationMethodsTable[idx].is_inmem_tuple_fn(relation,
+												 tup, fasttab_snapshot, idx))
+		return false;
+
+	oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+	heaptup->t_self = GenFasttabItemPointerData();
+	dlist_tup = palloc0(sizeof(DListHeapTupleData));
+	dlist_tup->tup = heap_copytuple(heaptup);
+	MemoryContextSwitchTo(oldctx);
+
+	dlist_push_tail(&fasttab_snapshot->relationData[idx].tuples,
+					&dlist_tup->node);
+	fasttab_snapshot->relationData[idx].tuples_num++;
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_insert, dlist_tup->tup->t_self = %08X/%04X, oid = %d, inmemory tuples num = %d, heaptup oid = %d, idx = %d, relation relid = %d",
+		 BlockIdGetBlockNumber(&dlist_tup->tup->t_self.ip_blkid),
+		 dlist_tup->tup->t_self.ip_posid, HeapTupleGetOid(dlist_tup->tup),
+		 fasttab_snapshot->relationData[idx].tuples_num,
+		 HeapTupleGetOid(heaptup), idx, RelationGetRelid(relation)
+		);
+#endif
+
+	CacheInvalidateHeapTuple(relation, heaptup, NULL);
+	pgstat_count_heap_insert(relation, 1);
+	if (heaptup != tup)
+	{
+		tup->t_self = heaptup->t_self;
+		heap_freetuple(heaptup);
+	}
+
+	*result = HeapTupleGetOid(tup);
+	return true;
+}
+
+/*
+ * Remove pg_depend and pg_type records that would be kept in memory otherwise
+ * when relation with given Oid is deleted.
+ */
+static void
+fasttab_clean_catalog_on_relation_delete(Oid reloid)
+{
+	Oid			curroid = reloid;
+	FasttabSnapshot fasttab_snapshot = FasttabSnapshotGetCurrent();
+	int			dependIdx = GetSnapshotRelationIdxByOid(DependRelationId);
+	int			typeIdx = GetSnapshotRelationIdxByOid(TypeRelationId);
+	Relation	dependRel = relation_open(DependRelationId, AccessShareLock);
+	Relation	typeRel = relation_open(TypeRelationId, AccessShareLock);
+	ItemPointerData itemPointerData;
+
+	for (;;)
+	{
+		dlist_iter	iter;
+		bool		isnull,
+					found = false;
+
+		/* Find pg_depend tuple with refobjid == curroid. */
+		dlist_foreach(iter, &fasttab_snapshot->relationData[dependIdx].tuples)
+		{
+			DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+			Oid			refobjid = DatumGetObjectId(heap_getattr(dlist_tup->tup, Anum_pg_depend_refobjid,
+									  RelationGetDescr(dependRel), &isnull));
+
+			if (refobjid == curroid)
+			{
+				found = true;
+				/* curroid := tuple.objid */
+				curroid = DatumGetObjectId(heap_getattr(dlist_tup->tup, Anum_pg_depend_objid,
+									  RelationGetDescr(dependRel), &isnull));
+
+				/*
+				 * Delete found tuple. Can't pass dlist_tup->tup->t_self as an
+				 * argument - this memory is about to be freed.
+				 */
+				itemPointerData = dlist_tup->tup->t_self;
+				fasttab_delete(dependRel, &itemPointerData);
+				break;
+			}
+		}
+
+		/* If not found - cleanup is done, end of loop */
+		if (!found)
+			break;
+
+		/* Find pg_type tuple with oid == curroid */
+		found = false;
+		dlist_foreach(iter, &fasttab_snapshot->relationData[typeIdx].tuples)
+		{
+			DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+			Oid			oid = DatumGetObjectId(heap_getattr(dlist_tup->tup, ObjectIdAttributeNumber,
+										RelationGetDescr(typeRel), &isnull));
+
+			if (oid == curroid)
+			{
+				found = true;
+
+				/*
+				 * Delete found tuple. Can't pass dlist_tup->tup->t_self as an
+				 * argument - this memory is about to be freed.
+				 */
+				itemPointerData = dlist_tup->tup->t_self;
+				fasttab_delete(typeRel, &itemPointerData);
+				break;
+			}
+		}
+
+		Assert(found);
+	}
+
+	relation_close(typeRel, AccessShareLock);
+	relation_close(dependRel, AccessShareLock);
+}
+
+/*
+ * on heap_delete
+ * true on success, false to proceeed as usual
+ */
+bool
+fasttab_delete(Relation relation, ItemPointer tid)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+
+	if (!IsFasttabItemPointer(tid))
+		return false;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(relation));
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+
+		if (ItemPointerEquals(&dlist_tup->tup->t_self, tid))
+		{
+			if (RelationGetRelid(relation) == RelationRelationId)
+			{
+				bool		isnull;
+				Oid			reloid = DatumGetObjectId(heap_getattr(dlist_tup->tup, ObjectIdAttributeNumber,
+									   RelationGetDescr(relation), &isnull));
+
+				fasttab_clean_catalog_on_relation_delete(reloid);
+			}
+
+			pgstat_count_heap_delete(relation);
+			CacheInvalidateHeapTuple(relation, dlist_tup->tup, NULL);
+
+			dlist_delete(&dlist_tup->node);
+			DListHeapTupleFree(dlist_tup);
+			fasttab_snapshot->relationData[idx].tuples_num--;
+
+#ifdef FASTTAB_DEBUG
+			elog(NOTICE, "FASTTAB: fasttab_delete, tid = %08X/%04X - entry found and deleted, tuples_num = %d, idx = %d, rd_id = %d",
+				 BlockIdGetBlockNumber(&tid->ip_blkid), tid->ip_posid,
+				 fasttab_snapshot->relationData[idx].tuples_num, idx, relation->rd_id
+				);
+#endif
+
+			return true;
+		}
+	}
+
+	elog(ERROR, "in-memory tuple not found during delete");
+	return false;				/* will be never reached */
+}
+
+/*
+ * on heap_update
+ * true on success, false to proceeed as usual
+ */
+bool
+fasttab_update(Relation relation, ItemPointer otid, HeapTuple newtup)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+
+	if (!IsFasttabItemPointer(otid))
+		return false;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_update, looking for otid = %08X/%04X",
+		 BlockIdGetBlockNumber(&otid->ip_blkid), otid->ip_posid);
+#endif
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(relation));
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+
+		if (ItemPointerEquals(&dlist_tup->tup->t_self, otid))
+		{
+			MemoryContext oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+
+			heap_freetuple(dlist_tup->tup);
+
+			/*
+			 * dont use old t_self for new tuple - it will cause an infinite
+			 * loop, I checked :)
+			 */
+			newtup->t_self = GenFasttabItemPointerData();
+			dlist_tup->tup = heap_copytuple(newtup);
+			MemoryContextSwitchTo(oldctx);
+
+			CacheInvalidateHeapTuple(relation, dlist_tup->tup, newtup);
+			pgstat_count_heap_update(relation, false);
+
+#ifdef FASTTAB_DEBUG
+			elog(NOTICE, "FASTTAB: fasttab_update - entry found and updated, newtup->t_self = %08X/%04X, oid = %d, tuples_num = %d, idx = %d",
+				 BlockIdGetBlockNumber(&newtup->t_self.ip_blkid), newtup->t_self.ip_posid,
+				 HeapTupleGetOid(dlist_tup->tup),
+				 fasttab_snapshot->relationData[idx].tuples_num, idx);
+#endif
+			return true;
+		}
+	}
+
+	elog(ERROR, "in-memory tuple not found during update");
+	return false;				/* will be never reached */
+}
+
+/*
+ * on heap_inplace_update
+ * true on success, false if proceed as usual
+ */
+bool
+fasttab_inplace_update(Relation relation, HeapTuple tuple)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+
+	if (!IsFasttabItemPointer(&tuple->t_self))
+		return false;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_heap_inplace_update, looking for tuple with tid = %08X/%04X, oid = %d...",
+	  BlockIdGetBlockNumber(&tuple->t_self.ip_blkid), tuple->t_self.ip_posid,
+		 HeapTupleGetOid(tuple));
+#endif
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(relation));
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+
+		if (ItemPointerEquals(&dlist_tup->tup->t_self, &tuple->t_self))
+		{
+			MemoryContext oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+
+			heap_freetuple(dlist_tup->tup);
+			dlist_tup->tup = heap_copytuple(tuple);
+			MemoryContextSwitchTo(oldctx);
+
+#ifdef FASTTAB_DEBUG
+			elog(NOTICE, "FASTTAB: fasttab_inplace_update - entry found and updated, tuples_num = %d, idx = %d",
+				 fasttab_snapshot->relationData[idx].tuples_num, idx);
+#endif
+			if (!IsBootstrapProcessingMode())
+				CacheInvalidateHeapTuple(relation, tuple, NULL);
+
+			return true;
+		}
+	}
+
+	elog(ERROR, "in-memory tuple not found (heap_inplace_update)");
+	return false;				/* will be never reached */
+}
+
+/*
+ * on index_insert
+ * true - override, false - continue
+ */
+bool
+fasttab_index_insert(Relation indexRelation, ItemPointer heap_t_ctid,
+					 bool *result)
+{
+	Oid			indexId = RelationGetRelid(indexRelation);
+
+	if (!IsFasttabItemPointer(heap_t_ctid))
+		return false;
+
+	Assert(IsFasttabHandledIndexId(indexId));
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_insert, indexRelation relid = %u, heap_t_ctid = %08X/%04X",
+		 RelationGetRelid(indexRelation),
+		 BlockIdGetBlockNumber(&heap_t_ctid->ip_blkid),
+		 heap_t_ctid->ip_posid);
+#endif
+
+	if (IsFasttabHandledIndexId(indexId))
+	{
+		*result = true;
+		return true;			/* don't actually modify index */
+	}
+
+	return false;
+}
+
+/*
+ * > 0 - first is >
+ *	 0 - tuples are equal
+ * < 0 - first is <
+ */
+static int
+fasttab_index_compare_tuples(HeapTuple first, HeapTuple second,
+							 IndexScanDesc scan)
+{
+	TupleDesc	tupledesc = RelationGetDescr(scan->heapRelation);
+	Datum		datum1,
+				datum2;
+	bool		isnull1,
+				isnull2;
+	int			i,
+				result = 0;
+
+	for (i = 0; i < scan->indexMethods->nattr; i++)
+	{
+		Assert(scan->indexMethods->attrCompareMethod[i] != CompareInvalid);
+		datum1 = heap_getattr(first, scan->indexMethods->attrNumbers[i], tupledesc,
+							  &isnull1);
+		datum2 = heap_getattr(second, scan->indexMethods->attrNumbers[i], tupledesc,
+							  &isnull2);
+		Assert((!isnull1) && (!isnull2));
+
+		switch (scan->indexMethods->attrCompareMethod[i])
+		{
+			case CompareOid:
+				result = FasttabCompareInts(DatumGetObjectId(datum1),
+											DatumGetObjectId(datum2));
+				break;
+			case CompareCString:
+				result = strcmp(DatumGetCString(datum1),
+								DatumGetCString(datum2));
+				break;
+			case CompareInt16:
+				result = FasttabCompareInts(DatumGetInt16(datum1),
+											DatumGetInt16(datum2));
+				break;
+			case CompareInt64:
+				result = FasttabCompareInts(DatumGetInt64(datum1),
+											DatumGetInt64(datum2));
+				break;
+			case CompareBoolean:
+				result = FasttabCompareInts(DatumGetBool(datum1),
+											DatumGetBool(datum2));
+				break;
+			default:			/* should never happen, can be useful during
+								 * development though */
+				elog(ERROR, "Unexpected compare method: %d",
+					 scan->indexMethods->attrCompareMethod[i]);
+		}
+
+		if (result != 0)
+			break;
+	}
+
+	return result;
+}
+
+/*
+ * for filling scan->xs_itup
+ * used during index-only scans
+ */
+static IndexTuple
+fasttab_index_form_tuple(HeapTuple tup, IndexScanDesc scan)
+{
+	TupleDesc	heaptupledesc = RelationGetDescr(scan->heapRelation);
+	TupleDesc	indextupledesc = RelationGetDescr(scan->indexRelation);
+	Datum		values[FasttabIndexMaxAttributes];
+	bool		isnull[FasttabIndexMaxAttributes];
+	int			i;
+
+	for (i = 0; i < scan->indexMethods->nattr; i++)
+
+		/*
+		 * NB: prcesses negative attribute numbers e.g.
+		 * ObjectIdAttributeNumber just fine
+		 */
+		values[i] = heap_getattr(tup, scan->indexMethods->attrNumbers[i],
+								 heaptupledesc, &(isnull[i]));
+
+	return index_form_tuple(indextupledesc, values, isnull);
+}
+
+static inline AttrNumber
+fasttab_convert_index_attno_to_heap_attno(IndexScanDesc scan,
+										  AttrNumber indexAttno)
+{
+	Assert(indexAttno > 0);
+	Assert(indexAttno <= FasttabIndexMaxAttributes);
+	Assert(indexAttno <= scan->indexMethods->nattr);
+	return scan->indexMethods->attrNumbers[indexAttno - 1];
+}
+
+static bool
+fasttab_index_tuple_matches_where_condition(IndexScanDesc scan, HeapTuple tup)
+{
+	int			i;
+	bool		insert;
+	AttrNumber	attrNumbersBackup[FasttabIndexMaxAttributes];
+
+	if (scan->numberOfKeys == 0)
+		return true;
+
+	/* NB: scan->keyData[0].sk_strategy can be InvalidStrategy */
+	Assert(scan->keyData != NULL);
+	Assert(scan->keyData[0].sk_attno != InvalidAttrNumber);
+
+	/* convert index attribute numbers to tuple attribute numbers */
+	for (i = 0; i < scan->numberOfKeys; i++)
+	{
+		attrNumbersBackup[i] = scan->keyData[i].sk_attno;
+		scan->keyData[i].sk_attno = fasttab_convert_index_attno_to_heap_attno(scan, scan->keyData[i].sk_attno);
+	}
+
+	/* NB: HeapKeyTest is a macro, it changes `insert` variable */
+	HeapKeyTest(tup, RelationGetDescr(scan->heapRelation), scan->numberOfKeys,
+				scan->keyData, insert);
+
+	/* restore original attribute numbers */
+	for (i = 0; i < scan->numberOfKeys; i++)
+		scan->keyData[i].sk_attno = attrNumbersBackup[i];
+
+	return insert;
+}
+
+/*
+ * basically insert-sort implementation
+ * true - tuple added
+ * false - tuple not added, filtered by WHERE condition
+ */
+static bool
+fasttab_index_insert_tuple_in_sorted_list(IndexScanDesc scan, HeapTuple tup)
+{
+	DListHeapTuple dlist_tup;
+	dlist_node *insert_after = &scan->xs_inmem_tuplist.head;
+	dlist_iter	iter;
+
+	/*
+	 * apparently scan->orderByData is never specified in index-only scans for
+	 * catalog tables
+	 */
+	Assert(scan->numberOfOrderBys == 0);
+	Assert(scan->numberOfKeys >= 0 && scan->numberOfKeys <= FasttabIndexMaxAttributes);
+
+	if (!fasttab_index_tuple_matches_where_condition(scan, tup))
+		return false;
+
+	dlist_tup = palloc(sizeof(DListHeapTupleData));
+	dlist_tup->tup = heap_copytuple(tup);
+
+	dlist_foreach(iter, &scan->xs_inmem_tuplist)
+	{
+		DListHeapTuple dlist_curr = (DListHeapTuple) iter.cur;
+
+		if (fasttab_index_compare_tuples(dlist_curr->tup, tup, scan) >= 0)
+			break;
+
+		insert_after = iter.cur;
+	}
+
+	dlist_insert_after(insert_after, &dlist_tup->node);
+
+	/* NB: apparently HeapTupleGetOid(tup) == InvalidOid (0) case is OK */
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_insert_tuple_in_sorted_list scan = %p, tup oid = %d, tuple added to list",
+		 scan, HeapTupleGetOid(tup));
+#endif
+
+	return true;
+}
+
+/*
+ * on index_geinscan_internal
+ * NB: scan->keyData is not initialized here (usually filled with 0x7f's)
+ */
+void
+fasttab_index_beginscan(IndexScanDesc scan)
+{
+	Oid			indexId = RelationGetRelid(scan->indexRelation);
+
+	Assert(PointerIsValid(scan->indexRelation));
+
+	if (!IsFasttabHandledIndexId(indexId))
+		return;
+
+	scan->xs_regular_tuple_enqueued = false;
+	scan->xs_regular_scan_finished = false;
+	scan->xs_scan_finish_returned = false;
+
+	/* indexMethods is accessed quite often so we memoize it */
+	scan->indexMethods = GetFasttabIndexMethods(indexId);
+
+	/*
+	 * Delayed initialization of scan->xs_inmem_tuplist is required when
+	 * fasttab_index_getnext_tid_merge is called first time.  The idea heare
+	 * is the same as in lazy evaluation 1) It's more efficient then
+	 * initializing a list here, since sometimes beginscan/rescan are called
+	 * without any scanning 2) We don't waste memory for tuples we don't need
+	 * since they will be filtered anyway 3) Besides sometimes `scan` is
+	 * passed to beginscan is not fully initilized so we can't actually filter
+	 * tuples by WHERE condition here
+	 */
+	scan->xs_inmem_tuplist_init_done = false;
+
+	dlist_init(&scan->xs_inmem_tuplist);
+
+	/*
+	 * Make sure scan->xs_ctup.t_self has proper initial value (required in
+	 * index_getnext_tid)
+	 */
+	ItemPointerSetInvalid(&scan->xs_ctup.t_self);
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_beginscan (could be called from rescan), scan = %p, indexId = %u "
+		 "scan->numberOfKeys = %d, scan->keyData = %p, scan->numberOfOrderBys = %d, scan->orderByData = %p",
+	scan, indexId, scan->numberOfKeys, scan->keyData, scan->numberOfOrderBys,
+		 scan->orderByData
+		);
+#endif
+
+}
+
+/* on index_endscan */
+void
+fasttab_index_endscan(IndexScanDesc scan)
+{
+	Assert(PointerIsValid(scan->indexRelation));
+
+	if (!IsFasttabHandledIndexId(RelationGetRelid(scan->indexRelation)))
+		return;
+
+	/* free in-memory tuples left */
+	FasttabDListFree(&scan->xs_inmem_tuplist);
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_endscan (could be called from rescan), scan = %p, scan->indexRelation relid = %u",
+		 scan, RelationGetRelid(scan->indexRelation)
+		);
+#endif
+
+}
+
+/*
+ * on index_rescan
+ * NB: scan->keyData is not initialized here (usually filled with 0x7f's)
+ */
+void
+fasttab_index_rescan(IndexScanDesc scan, ScanKey keys, int nkeys,
+					 ScanKey orderbys, int norderbys)
+{
+	fasttab_index_endscan(scan);
+	fasttab_index_beginscan(scan);
+}
+
+/*
+ * almost as heap_fetch with keep_buf = true, but also understands HOT chains
+ * true - tuple found
+ * false - tuple not found
+ */
+bool
+fasttab_simple_heap_fetch(Relation relation, Snapshot snapshot,
+						  HeapTuple tuple)
+{
+	Page		page;
+	bool		found;
+	Buffer		buffer = InvalidBuffer;
+	ItemPointer tid = &(tuple->t_self);
+
+	/*
+	 * No need to lock any buffers for in-memory tuple, they could not even
+	 * exist!
+	 */
+	if (IsFasttabItemPointer(tid))
+		return heap_hot_search_buffer(tid, relation, buffer, snapshot, tuple, NULL, true);
+
+	/* Fetch and pin the appropriate page of the relation. */
+	buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(tid));
+
+	/* Need share lock on buffer to examine tuple commit status. */
+	LockBuffer(buffer, BUFFER_LOCK_SHARE);
+	page = BufferGetPage(buffer);
+	TestForOldSnapshot(snapshot, relation, page);
+
+	found = heap_hot_search_buffer(tid, relation, buffer, snapshot, tuple,
+								   NULL, true);
+
+	LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
+	ReleaseBuffer(buffer);
+
+	return found;
+}
+
+static void
+fasttab_index_make_sure_inmem_tuplist_init_done(IndexScanDesc scan)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+
+	Assert(PointerIsValid(scan->indexRelation));
+
+	/* initialize scan->xs_inmem_tuplist during first call */
+	if (scan->xs_inmem_tuplist_init_done)
+		return;
+
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(scan->heapRelation));
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_curr = (DListHeapTuple) iter.cur;
+
+		(void) fasttab_index_insert_tuple_in_sorted_list(scan, dlist_curr->tup);
+	}
+
+	scan->xs_inmem_tuplist_init_done = true;
+}
+
+/*
+ * on index_getnext_tid
+ * if found == true, &scan->xs_ctup.t_self is a regular current ItemPointer
+ * save resulting ItemPointer to &scan->xs_ctup.t_self
+ * NB: we filter tuples using scan->keyData HERE since it's not always
+ * initialized when fasttab_index_beginscan or _rescan is called (usually
+ * filled with 0x7f's)
+ */
+bool
+fasttab_index_getnext_tid_merge(IndexScanDesc scan, ScanDirection direction)
+{
+	bool		fetched;
+	DListHeapTuple ret_node;
+
+	Assert(PointerIsValid(scan->indexRelation));
+
+	if (!IsFasttabHandledIndexId(RelationGetRelid(scan->indexRelation)))
+		/* regular logic */
+		return scan->indexRelation->rd_amroutine->amgettuple(scan, direction);
+
+	/* initialize scan->xs_inmem_tuplist during first call */
+	fasttab_index_make_sure_inmem_tuplist_init_done(scan);
+
+	if (dlist_is_empty(&scan->xs_inmem_tuplist))		/* in-memory tuples
+														 * enumiration is over? */
+	{
+#ifdef FASTTAB_DEBUG
+		elog(NOTICE, "FASTTAB: fasttab_index_getnext_tid_merge, scan = %p, fake tuples list is empty, xs_regular_scan_finished = %u, xs_scan_finish_returned = %u",
+		scan, scan->xs_regular_scan_finished, scan->xs_scan_finish_returned);
+#endif
+
+		/*
+		 * If ->amgettuple() already returned false we should not call it once
+		 * again.  In this case btree index will start a scan all over again,
+		 * see btgettuple implementation.  Still if user will call this
+		 * procedure once again dispite of returned 'false' value she probably
+		 * knows what she is doing.
+		 */
+		if (scan->xs_regular_scan_finished && (!scan->xs_scan_finish_returned))
+		{
+			scan->xs_scan_finish_returned = true;
+			return false;
+		}
+
+		/* regular logic */
+		return scan->indexRelation->rd_amroutine->amgettuple(scan, direction);
+	}
+
+	/*
+	 * Apparently other directions are not used in index-only scans for
+	 * catalog tables. No need to check direction above this point since only
+	 * here scan->xs_inmem_tuplist is both initialized and non-empty.
+	 */
+	Assert(ScanDirectionIsForward(direction));
+
+	/* there is no regular tuple in in-memory queue, we should load one */
+	while ((!scan->xs_regular_tuple_enqueued) && (!scan->xs_regular_scan_finished))
+	{
+		if (scan->indexRelation->rd_amroutine->amgettuple(scan, direction))
+		{
+			HeapTupleData regular_tup;
+
+			regular_tup.t_self = scan->xs_ctup.t_self;
+			fetched = fasttab_simple_heap_fetch(scan->heapRelation, scan->xs_snapshot,
+												&regular_tup);
+
+			if (!fetched)
+			{
+#ifdef FASTTAB_DEBUG
+				elog(NOTICE, "FASTTAB: fasttab_index_getnext_tid_merge, scan = %p, indexed tuple not found, 'continue;'",
+					 scan);
+#endif
+				continue;
+			}
+			scan->xs_regular_tuple_enqueued = fasttab_index_insert_tuple_in_sorted_list(scan, &regular_tup);
+		}
+		else
+			scan->xs_regular_scan_finished = true;
+	}
+
+	Assert(scan->xs_regular_scan_finished || scan->xs_regular_tuple_enqueued);
+
+	ret_node = (DListHeapTuple) dlist_pop_head_node(&scan->xs_inmem_tuplist);
+	Assert(PointerIsValid(ret_node));
+
+	scan->xs_recheck = false;	/* see 'system catalog scans with lossy index
+								 * conditions are not implemented' in genam.c */
+
+	/*
+	 * we could write `heap_copytuple_with_tuple(ret_node->tup,
+	 * &scan->xs_ctup)` here as well
+	 */
+	ItemPointerCopy(&ret_node->tup->t_self, &scan->xs_ctup.t_self);
+
+	if (!IsFasttabItemPointer(&scan->xs_ctup.t_self))
+		scan->xs_regular_tuple_enqueued = false;
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_getnext_tid_merge, scan = %p, direction = %d, scan->indexRelation relid = %u, return tuple tid = %08X/%04X",
+		 scan, direction, RelationGetRelid(scan->indexRelation),
+		 BlockIdGetBlockNumber(&scan->xs_ctup.t_self.ip_blkid),
+		 scan->xs_ctup.t_self.ip_posid
+		);
+#endif
+
+	/* scan->xs_itup should not be NULL! */
+	scan->xs_itup = fasttab_index_form_tuple(ret_node->tup, scan);
+
+	DListHeapTupleFree(ret_node);
+	return true;
+}
+
+/*
+ * on index_getmap
+ * true - override done
+ * fasle - use regular logic
+ */
+bool
+fasttab_index_getbitmap(IndexScanDesc scan, TIDBitmap *bitmap, int64 *result)
+{
+	int64		ntids = 0;
+	bool		heap_opened = false;
+
+	Assert(PointerIsValid(scan->indexRelation));
+
+	if (!IsFasttabHandledIndexId(RelationGetRelid(scan->indexRelation)))
+		return false;
+
+	/* fill scan->heapRelation if it's NULL, we require it in our hooks */
+	if (!scan->heapRelation)
+	{
+		scan->heapRelation = heap_open(scan->indexRelation->rd_index->indrelid,
+									   NoLock);
+		heap_opened = true;
+	}
+
+	/* initialize scan->xs_inmem_tuplist during first call */
+	fasttab_index_make_sure_inmem_tuplist_init_done(scan);
+
+	if (dlist_is_empty(&scan->xs_inmem_tuplist))		/* there are if fact no
+														 * in-memory tuples? */
+	{
+		if (heap_opened)		/* cleanup */
+		{
+			heap_close(scan->heapRelation, NoLock);
+			scan->heapRelation = NULL;
+		}
+		return false;
+	}
+
+	while (fasttab_index_getnext_tid_merge(scan, ForwardScanDirection))
+	{
+		tbm_add_tuples(bitmap, &scan->xs_ctup.t_self, 1, false);
+		ntids++;
+	}
+
+	if (heap_opened)			/* cleanup */
+	{
+		heap_close(scan->heapRelation, NoLock);
+		scan->heapRelation = NULL;
+	}
+
+	*result = ntids;
+	return true;
+}
+
+
+/*****************************************************************************
+			   PROCEDURES USED IN FasttabRelationMethodsTable
+ *****************************************************************************/
+
+static bool
+generic_is_inmem_tuple(Relation relation, HeapTuple tup,
+					   FasttabSnapshot fasttab_snapshot, int tableIdx)
+{
+	dlist_iter	iter;
+	TupleDesc	tupledesc;
+	Oid			values[FasttabRelationMaxOidAttributes];
+	bool		isnull;
+	int			i,
+				pg_class_idx,
+				noidattr = FasttabRelationMethodsTable[tableIdx].noidattr;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+	Assert(tableIdx >= 0 && tableIdx < FasttabSnapshotTablesNumber);
+	Assert(noidattr > 0 && noidattr <= FasttabRelationMaxOidAttributes);
+
+	/*
+	 * Special case. During table creation pg_type and pg_depend are modified
+	 * before pg_class (see heap_create_with_catalog implementation) so there
+	 * is no way to tell wheter tuples are in-memory without using
+	 * relperistence hint. Also this check could be considered as an
+	 * optimization.
+	 */
+	if ((RelationGetRelid(relation) == TypeRelationId) || (RelationGetRelid(relation) == DependRelationId))
+		return (CurrentRelpersistenceHint == RELPERSISTENCE_FAST_TEMP);
+
+	tupledesc = RelationGetDescr(relation);
+
+	for (i = 0; i < noidattr; i++)
+	{
+		values[i] = DatumGetObjectId(heap_getattr(tup,
+						FasttabRelationMethodsTable[tableIdx].attrNumbers[i],
+												  tupledesc, &isnull));
+		Assert(!isnull);
+	}
+
+	/*
+	 * Check whether there is an in-memory pg_class tuple with oid from
+	 * values[] array
+	 */
+	pg_class_idx = GetSnapshotRelationIdxByOid(RelationRelationId);
+	dlist_foreach(iter, &fasttab_snapshot->relationData[pg_class_idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+		Oid			oid = HeapTupleGetOid(dlist_tup->tup);
+
+		for (i = 0; i < noidattr; i++)
+		{
+			if (oid == values[i])
+				return true;
+		}
+	}
+
+	return false;
+}
+
+static bool
+pg_class_is_inmem_tuple(Relation relation, HeapTuple tup,
+						FasttabSnapshot fasttab_snapshot, int tableIdx)
+{
+	bool		isnull;
+	Datum		relpersistencedat;
+	TupleDesc	tupledesc;
+
+	Assert(RelationGetRelid(relation) == RelationRelationId);
+
+	tupledesc = RelationGetDescr(relation);
+	relpersistencedat = heap_getattr(tup, Anum_pg_class_relpersistence,
+									 tupledesc, &isnull);
+	Assert(!isnull);
+	return ((char) relpersistencedat == RELPERSISTENCE_FAST_TEMP);
+}
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index fac166d..d952512 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -846,7 +846,8 @@ gistGetFakeLSN(Relation rel)
 {
 	static XLogRecPtr counter = 1;
 
-	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		rel->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP)
 	{
 		/*
 		 * Temporary relations are only accessible in our session, so a simple
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 38bba16..779af83 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -53,6 +53,7 @@
 #include "access/xlog.h"
 #include "access/xloginsert.h"
 #include "access/xlogutils.h"
+#include "access/fasttab.h"
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "miscadmin.h"
@@ -72,6 +73,8 @@
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 #include "utils/tqual.h"
+#include "utils/memutils.h"
+#include "lib/ilist.h"
 
 
 /* GUC variable */
@@ -1505,6 +1508,7 @@ heap_beginscan_internal(Relation relation, Snapshot snapshot,
 		scan->rs_key = NULL;
 
 	initscan(scan, key, false);
+	fasttab_beginscan(scan);
 
 	return scan;
 }
@@ -1546,6 +1550,8 @@ heap_rescan(HeapScanDesc scan,
 		parallel_scan->phs_cblock = parallel_scan->phs_startblock;
 		SpinLockRelease(&parallel_scan->phs_mutex);
 	}
+
+	fasttab_beginscan(scan);
 }
 
 /* ----------------
@@ -1779,6 +1785,11 @@ retry:
 HeapTuple
 heap_getnext(HeapScanDesc scan, ScanDirection direction)
 {
+	HeapTuple	fasttab_result = fasttab_getnext(scan, direction);
+
+	if (HeapTupleIsValid(fasttab_result))
+		return fasttab_result;
+
 	/* Note: no locking manipulations needed */
 
 	HEAPDEBUG_1;				/* heap_getnext( info ) */
@@ -1859,6 +1870,8 @@ heap_fetch(Relation relation,
 	OffsetNumber offnum;
 	bool		valid;
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	/*
 	 * Fetch and pin the appropriate page of the relation.
 	 */
@@ -1984,12 +1997,22 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
 					   Snapshot snapshot, HeapTuple heapTuple,
 					   bool *all_dead, bool first_call)
 {
-	Page		dp = (Page) BufferGetPage(buffer);
+	Page		dp;
 	TransactionId prev_xmax = InvalidTransactionId;
 	OffsetNumber offnum;
 	bool		at_chain_start;
 	bool		valid;
 	bool		skip;
+	bool		fasttab_result;
+
+	if (fasttab_hot_search_buffer(tid, relation, heapTuple, all_dead, &fasttab_result))
+		return fasttab_result;
+
+	/*
+     * `buffer` can be InvalidBuffer for in-memory tuples, so we should call
+     * BufferGetPage only after we verified it's not a case.
+     */
+	dp = (Page) BufferGetPage(buffer);
 
 	/* If this is not the first call, previous call returned a (live!) tuple */
 	if (all_dead)
@@ -2158,6 +2181,8 @@ heap_get_latest_tid(Relation relation,
 	ItemPointerData ctid;
 	TransactionId priorXmax;
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	/* this is to avoid Assert failures on bad input */
 	if (!ItemPointerIsValid(tid))
 		return;
@@ -2376,6 +2401,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
 	Buffer		buffer;
 	Buffer		vmbuffer = InvalidBuffer;
 	bool		all_visible_cleared = false;
+	Oid			fasttab_result;
 
 	/*
 	 * Fill in tuple header fields, assign an OID, and toast the tuple if
@@ -2386,6 +2412,9 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
 	 */
 	heaptup = heap_prepare_insert(relation, tup, xid, cid, options);
 
+	if (fasttab_insert(relation, tup, heaptup, &fasttab_result))
+		return fasttab_result;
+
 	/*
 	 * Find buffer to insert this tuple into.  If the page is all visible,
 	 * this will also pin the requisite visibility map page.
@@ -2644,6 +2673,8 @@ heap_multi_insert(Relation relation, HeapTuple *tuples, int ntuples,
 	bool		need_tuple_data = RelationIsLogicallyLogged(relation);
 	bool		need_cids = RelationIsAccessibleInLogicalDecoding(relation);
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	needwal = !(options & HEAP_INSERT_SKIP_WAL) && RelationNeedsWAL(relation);
 	saveFreeSpace = RelationGetTargetPageFreeSpace(relation,
 												   HEAP_DEFAULT_FILLFACTOR);
@@ -3006,6 +3037,9 @@ heap_delete(Relation relation, ItemPointer tid,
 
 	Assert(ItemPointerIsValid(tid));
 
+	if (fasttab_delete(relation, tid))
+		return HeapTupleMayBeUpdated;
+
 	/*
 	 * Forbid this during a parallel operation, lest it allocate a combocid.
 	 * Other workers might need that combocid for visibility checks, and we
@@ -3488,6 +3522,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot update tuples during a parallel operation")));
 
+
+	if (fasttab_update(relation, otid, newtup))
+		return HeapTupleMayBeUpdated;
+
 	/*
 	 * Fetch the list of attributes to be checked for HOT update.  This is
 	 * wasted effort if we fail to update or have to put the new tuple on a
@@ -4581,6 +4619,8 @@ heap_lock_tuple(Relation relation, HeapTuple tuple,
 	bool		have_tuple_lock = false;
 	bool		cleared_all_frozen = false;
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	*buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(tid));
 	block = ItemPointerGetBlockNumber(tid);
 
@@ -5212,6 +5252,8 @@ static bool
 heap_acquire_tuplock(Relation relation, ItemPointer tid, LockTupleMode mode,
 					 LockWaitPolicy wait_policy, bool *have_tuple_lock)
 {
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	if (*have_tuple_lock)
 		return true;
 
@@ -5904,6 +5946,8 @@ static HTSU_Result
 heap_lock_updated_tuple(Relation rel, HeapTuple tuple, ItemPointer ctid,
 						TransactionId xid, LockTupleMode mode)
 {
+	Assert(!IsFasttabHandledRelationId(rel->rd_id));
+
 	if (!ItemPointerEquals(&tuple->t_self, ctid))
 	{
 		/*
@@ -5949,6 +5993,8 @@ heap_finish_speculative(Relation relation, HeapTuple tuple)
 	ItemId		lp = NULL;
 	HeapTupleHeader htup;
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(&(tuple->t_self)));
 	LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
 	page = (Page) BufferGetPage(buffer);
@@ -6042,6 +6088,7 @@ heap_abort_speculative(Relation relation, HeapTuple tuple)
 	Buffer		buffer;
 
 	Assert(ItemPointerIsValid(tid));
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
 
 	block = ItemPointerGetBlockNumber(tid);
 	buffer = ReadBuffer(relation, block);
@@ -6179,6 +6226,9 @@ heap_inplace_update(Relation relation, HeapTuple tuple)
 	uint32		oldlen;
 	uint32		newlen;
 
+	if (fasttab_inplace_update(relation, tuple))
+		return;
+
 	/*
 	 * For now, parallel operations are required to be strictly read-only.
 	 * Unlike a regular update, this should never create a combo CID, so it
@@ -6553,6 +6603,8 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
 	TransactionId xid;
 	bool		totally_frozen = true;
 
+	Assert(!IsFasttabItemPointer(&tuple->t_ctid));
+
 	frz->frzflags = 0;
 	frz->t_infomask2 = tuple->t_infomask2;
 	frz->t_infomask = tuple->t_infomask;
@@ -6723,6 +6775,8 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
 void
 heap_execute_freeze_tuple(HeapTupleHeader tuple, xl_heap_freeze_tuple *frz)
 {
+	Assert(!IsFasttabItemPointer(&tuple->t_ctid));
+
 	HeapTupleHeaderSetXmax(tuple, frz->xmax);
 
 	if (frz->frzflags & XLH_FREEZE_XVAC)
@@ -7125,6 +7179,8 @@ heap_tuple_needs_eventual_freeze(HeapTupleHeader tuple)
 {
 	TransactionId xid;
 
+	Assert(!IsFasttabItemPointer(&tuple->t_ctid));
+
 	/*
 	 * If xmin is a normal transaction ID, this tuple is definitely not
 	 * frozen.
@@ -7179,6 +7235,8 @@ heap_tuple_needs_freeze(HeapTupleHeader tuple, TransactionId cutoff_xid,
 {
 	TransactionId xid;
 
+	Assert(!IsFasttabItemPointer(&tuple->t_ctid));
+
 	xid = HeapTupleHeaderGetXmin(tuple);
 	if (TransactionIdIsNormal(xid) &&
 		TransactionIdPrecedes(xid, cutoff_xid))
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 54b71cb..a56b0c5 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -69,6 +69,7 @@
 #include "access/relscan.h"
 #include "access/transam.h"
 #include "access/xlog.h"
+#include "access/fasttab.h"
 #include "catalog/catalog.h"
 #include "catalog/index.h"
 #include "pgstat.h"
@@ -193,9 +194,14 @@ index_insert(Relation indexRelation,
 			 Relation heapRelation,
 			 IndexUniqueCheck checkUnique)
 {
+	bool		result;
+
 	RELATION_CHECKS;
 	CHECK_REL_PROCEDURE(aminsert);
 
+	if (fasttab_index_insert(indexRelation, heap_t_ctid, &result))
+		return result;
+
 	if (!(indexRelation->rd_amroutine->ampredlocks))
 		CheckForSerializableConflictIn(indexRelation,
 									   (HeapTuple) NULL,
@@ -262,6 +268,7 @@ static IndexScanDesc
 index_beginscan_internal(Relation indexRelation,
 						 int nkeys, int norderbys, Snapshot snapshot)
 {
+	IndexScanDesc result;
 	RELATION_CHECKS;
 	CHECK_REL_PROCEDURE(ambeginscan);
 
@@ -276,8 +283,11 @@ index_beginscan_internal(Relation indexRelation,
 	/*
 	 * Tell the AM to open a scan.
 	 */
-	return indexRelation->rd_amroutine->ambeginscan(indexRelation, nkeys,
-													norderbys);
+	result = indexRelation->rd_amroutine->ambeginscan(indexRelation, nkeys,
+													  norderbys);
+	fasttab_index_beginscan(result);
+
+	return result;
 }
 
 /* ----------------
@@ -316,6 +326,8 @@ index_rescan(IndexScanDesc scan,
 
 	scan->indexRelation->rd_amroutine->amrescan(scan, keys, nkeys,
 												orderbys, norderbys);
+
+	fasttab_index_rescan(scan, keys, nkeys, orderbys, norderbys);
 }
 
 /* ----------------
@@ -328,6 +340,8 @@ index_endscan(IndexScanDesc scan)
 	SCAN_CHECKS;
 	CHECK_SCAN_PROCEDURE(amendscan);
 
+	fasttab_index_endscan(scan);
+
 	/* Release any held pin on a heap page */
 	if (BufferIsValid(scan->xs_cbuf))
 	{
@@ -378,6 +392,7 @@ void
 index_restrpos(IndexScanDesc scan)
 {
 	Assert(IsMVCCSnapshot(scan->xs_snapshot));
+	Assert(!IsFasttabHandledIndexId(scan->indexRelation->rd_id));
 
 	SCAN_CHECKS;
 	CHECK_SCAN_PROCEDURE(amrestrpos);
@@ -412,7 +427,7 @@ index_getnext_tid(IndexScanDesc scan, ScanDirection direction)
 	 * scan->xs_recheck and possibly scan->xs_itup, though we pay no attention
 	 * to those fields here.
 	 */
-	found = scan->indexRelation->rd_amroutine->amgettuple(scan, direction);
+	found = fasttab_index_getnext_tid_merge(scan, direction);
 
 	/* Reset kill flag immediately for safety */
 	scan->kill_prior_tuple = false;
@@ -460,6 +475,16 @@ index_fetch_heap(IndexScanDesc scan)
 	bool		all_dead = false;
 	bool		got_heap_tuple;
 
+	if (IsFasttabItemPointer(tid))
+	{
+		bool		fasttab_result;
+
+		/* just get in-memory tuple by tid */
+		got_heap_tuple = fasttab_hot_search_buffer(tid, scan->heapRelation, &scan->xs_ctup, &all_dead, &fasttab_result);
+		Assert(got_heap_tuple && fasttab_result);
+		return &scan->xs_ctup;
+	}
+
 	/* We can skip the buffer-switching logic if we're in mid-HOT chain. */
 	if (!scan->xs_continue_hot)
 	{
@@ -594,10 +619,10 @@ index_getbitmap(IndexScanDesc scan, TIDBitmap *bitmap)
 	/* just make sure this is false... */
 	scan->kill_prior_tuple = false;
 
-	/*
-	 * have the am's getbitmap proc do all the work.
-	 */
-	ntids = scan->indexRelation->rd_amroutine->amgetbitmap(scan, bitmap);
+	/* try fasttab_ hook first ... */
+	if (!fasttab_index_getbitmap(scan, bitmap, &ntids))
+		/* if it failed - have the am's getbitmap proc do all the work. */
+		ntids = scan->indexRelation->rd_amroutine->amgetbitmap(scan, bitmap);
 
 	pgstat_count_index_tuples(scan->indexRelation, ntids);
 
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index ef69290..b081165 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -19,6 +19,7 @@
 #include "access/nbtree.h"
 #include "access/transam.h"
 #include "access/xloginsert.h"
+#include "access/fasttab.h"
 #include "miscadmin.h"
 #include "storage/lmgr.h"
 #include "storage/predicate.h"
@@ -330,6 +331,13 @@ _bt_check_unique(Relation rel, IndexTuple itup, Relation heapRel,
 					TransactionId xwait;
 
 					/*
+					 * If its in-memory tuple there is for sure no transaction
+					 * to wait for.
+					 */
+					if (IsFasttabItemPointer(&htid))
+						return InvalidTransactionId;
+
+					/*
 					 * It is a duplicate. If we are only doing a partial
 					 * check, then don't bother checking if the tuple is being
 					 * updated in another transaction. Just return the fact
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 23f36ea..e21faf1 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -30,6 +30,7 @@
 #include "access/xlog.h"
 #include "access/xloginsert.h"
 #include "access/xlogutils.h"
+#include "access/fasttab.h"
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/storage.h"
@@ -1928,6 +1929,8 @@ StartTransaction(void)
 	s->state = TRANS_INPROGRESS;
 
 	ShowTransactionState("StartTransaction");
+
+	fasttab_begin_transaction();
 }
 
 
@@ -2165,6 +2168,8 @@ CommitTransaction(void)
 	 */
 	s->state = TRANS_DEFAULT;
 
+	fasttab_end_transaction();
+
 	RESUME_INTERRUPTS();
 }
 
@@ -2611,6 +2616,8 @@ AbortTransaction(void)
 		pgstat_report_xact_timestamp(0);
 	}
 
+	fasttab_abort_transaction();
+
 	/*
 	 * State remains TRANS_ABORT until CleanupTransaction().
 	 */
@@ -3795,6 +3802,8 @@ DefineSavepoint(char *name)
 				 BlockStateAsString(s->blockState));
 			break;
 	}
+
+	fasttab_define_savepoint(name);
 }
 
 /*
@@ -4034,6 +4043,8 @@ RollbackToSavepoint(List *options)
 	else
 		elog(FATAL, "RollbackToSavepoint: unexpected state %s",
 			 BlockStateAsString(xact->blockState));
+
+	fasttab_rollback_to_savepoint(name);
 }
 
 /*
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 1baaa0b..76b9d4c 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -390,6 +390,7 @@ GetNewRelFileNode(Oid reltablespace, Relation pg_class, char relpersistence)
 	switch (relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			backend = BackendIdForTempRelations();
 			break;
 		case RELPERSISTENCE_UNLOGGED:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 04d7840..8984a7a 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -16,6 +16,7 @@
 
 #include "access/htup_details.h"
 #include "access/xact.h"
+#include "access/fasttab.h"
 #include "catalog/dependency.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
@@ -583,6 +584,13 @@ findDependentObjects(const ObjectAddress *object,
 	{
 		Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(tup);
 
+		/*
+		 * just ignore in-memory tuples here, they are properly handled in
+		 * fasttab.c already
+		 */
+		if (IsFasttabItemPointer(&tup->t_self))
+			continue;
+
 		otherObject.classId = foundDep->refclassid;
 		otherObject.objectId = foundDep->refobjid;
 		otherObject.objectSubId = foundDep->refobjsubid;
@@ -760,6 +768,13 @@ findDependentObjects(const ObjectAddress *object,
 		Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(tup);
 		int			subflags;
 
+		/*
+		 * just ignore in-memory tuples here, they are properly handled in
+		 * fasttab.c already
+		 */
+		if (IsFasttabItemPointer(&tup->t_self))
+			continue;
+
 		otherObject.classId = foundDep->classid;
 		otherObject.objectId = foundDep->objid;
 		otherObject.objectSubId = foundDep->objsubid;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index e997b57..0eef1ee 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -35,6 +35,7 @@
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog.h"
+#include "access/fasttab.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
@@ -996,7 +997,7 @@ AddNewRelationType(const char *typeName,
  *	tupdesc: tuple descriptor (source of column definitions)
  *	cooked_constraints: list of precooked check constraints and defaults
  *	relkind: relkind for new rel
- *	relpersistence: rel's persistence status (permanent, temp, or unlogged)
+ *	relpersistence: rel's persistence status (permanent, temp, fast temp or unlogged)
  *	shared_relation: TRUE if it's to be a shared relation
  *	mapped_relation: TRUE if the relation will use the relfilenode map
  *	oidislocal: TRUE if oid column (if any) should be marked attislocal
@@ -1184,70 +1185,101 @@ heap_create_with_catalog(const char *relname,
 							  relkind == RELKIND_COMPOSITE_TYPE))
 		new_array_oid = AssignTypeArrayOid();
 
-	/*
-	 * Since defining a relation also defines a complex type, we add a new
-	 * system type corresponding to the new relation.  The OID of the type can
-	 * be preselected by the caller, but if reltypeid is InvalidOid, we'll
-	 * generate a new OID for it.
-	 *
-	 * NOTE: we could get a unique-index failure here, in case someone else is
-	 * creating the same type name in parallel but hadn't committed yet when
-	 * we checked for a duplicate name above.
-	 */
-	new_type_addr = AddNewRelationType(relname,
-									   relnamespace,
-									   relid,
-									   relkind,
-									   ownerid,
-									   reltypeid,
-									   new_array_oid);
-	new_type_oid = new_type_addr.objectId;
-	if (typaddress)
-		*typaddress = new_type_addr;
-
-	/*
-	 * Now make the array type if wanted.
-	 */
-	if (OidIsValid(new_array_oid))
+	PG_TRY();
 	{
-		char	   *relarrayname;
+		/*
+		 * Usualy to figure out wheter tuple should be stored in-memory or not
+		 * we use in-memory part of pg_class table. Unfortunately during table
+		 * creation some tuples are stored in catalog tables before
+		 * modification of pg_class table. So there is no way to tell that
+		 * these tuples should be in-memory.
+		 *
+		 * Thus we set a hint with relperistence value of a table we about to
+		 * create. This not only solves a problem described above but also
+		 * allows to run described check much faster.
+		 */
+		fasttab_set_relpersistence_hint(relpersistence);
 
-		relarrayname = makeArrayTypeName(relname, relnamespace);
+		/*
+		 * Since defining a relation also defines a complex type, we add a new
+		 * system type corresponding to the new relation.  The OID of the type
+		 * can be preselected by the caller, but if reltypeid is InvalidOid,
+		 * we'll generate a new OID for it.
+		 *
+		 * NOTE: we could get a unique-index failure here, in case someone
+		 * else is creating the same type name in parallel but hadn't
+		 * committed yet when we checked for a duplicate name above.
+		 */
+		new_type_addr = AddNewRelationType(relname,
+										   relnamespace,
+										   relid,
+										   relkind,
+										   ownerid,
+										   reltypeid,
+										   new_array_oid);
 
-		TypeCreate(new_array_oid,		/* force the type's OID to this */
-				   relarrayname,	/* Array type name */
-				   relnamespace,	/* Same namespace as parent */
-				   InvalidOid,	/* Not composite, no relationOid */
-				   0,			/* relkind, also N/A here */
-				   ownerid,		/* owner's ID */
-				   -1,			/* Internal size (varlena) */
-				   TYPTYPE_BASE,	/* Not composite - typelem is */
-				   TYPCATEGORY_ARRAY,	/* type-category (array) */
-				   false,		/* array types are never preferred */
-				   DEFAULT_TYPDELIM,	/* default array delimiter */
-				   F_ARRAY_IN,	/* array input proc */
-				   F_ARRAY_OUT, /* array output proc */
-				   F_ARRAY_RECV,	/* array recv (bin) proc */
-				   F_ARRAY_SEND,	/* array send (bin) proc */
-				   InvalidOid,	/* typmodin procedure - none */
-				   InvalidOid,	/* typmodout procedure - none */
-				   F_ARRAY_TYPANALYZE,	/* array analyze procedure */
-				   new_type_oid,	/* array element type - the rowtype */
-				   true,		/* yes, this is an array type */
-				   InvalidOid,	/* this has no array type */
-				   InvalidOid,	/* domain base type - irrelevant */
-				   NULL,		/* default value - none */
-				   NULL,		/* default binary representation */
-				   false,		/* passed by reference */
-				   'd',			/* alignment - must be the largest! */
-				   'x',			/* fully TOASTable */
-				   -1,			/* typmod */
-				   0,			/* array dimensions for typBaseType */
-				   false,		/* Type NOT NULL */
-				   InvalidOid); /* rowtypes never have a collation */
+		fasttab_clear_relpersistence_hint();
 
-		pfree(relarrayname);
+		new_type_oid = new_type_addr.objectId;
+		if (typaddress)
+			*typaddress = new_type_addr;
+
+		/*
+		 * Now make the array type if wanted.
+		 */
+		if (OidIsValid(new_array_oid))
+		{
+			char	   *relarrayname;
+
+			relarrayname = makeArrayTypeName(relname, relnamespace);
+
+			fasttab_set_relpersistence_hint(relpersistence);
+
+			TypeCreate(new_array_oid,	/* force the type's OID to this */
+					   relarrayname,	/* Array type name */
+					   relnamespace,	/* Same namespace as parent */
+					   InvalidOid,		/* Not composite, no relationOid */
+					   0,		/* relkind, also N/A here */
+					   ownerid, /* owner's ID */
+					   -1,		/* Internal size (varlena) */
+					   TYPTYPE_BASE,	/* Not composite - typelem is */
+					   TYPCATEGORY_ARRAY,		/* type-category (array) */
+					   false,	/* array types are never preferred */
+					   DEFAULT_TYPDELIM,		/* default array delimiter */
+					   F_ARRAY_IN,		/* array input proc */
+					   F_ARRAY_OUT,		/* array output proc */
+					   F_ARRAY_RECV,	/* array recv (bin) proc */
+					   F_ARRAY_SEND,	/* array send (bin) proc */
+					   InvalidOid,		/* typmodin procedure - none */
+					   InvalidOid,		/* typmodout procedure - none */
+					   F_ARRAY_TYPANALYZE,		/* array analyze procedure */
+					   new_type_oid,	/* array element type - the rowtype */
+					   true,	/* yes, this is an array type */
+					   InvalidOid,		/* this has no array type */
+					   InvalidOid,		/* domain base type - irrelevant */
+					   NULL,	/* default value - none */
+					   NULL,	/* default binary representation */
+					   false,	/* passed by reference */
+					   'd',		/* alignment - must be the largest! */
+					   'x',		/* fully TOASTable */
+					   -1,		/* typmod */
+					   0,		/* array dimensions for typBaseType */
+					   false,	/* Type NOT NULL */
+					   InvalidOid);		/* rowtypes never have a collation */
+
+			fasttab_clear_relpersistence_hint();
+			pfree(relarrayname);
+		}
+
+	}
+	PG_CATCH();
+	{
+		/* clear relpersistence hint in case of error */
+		fasttab_clear_relpersistence_hint();
+		PG_RE_THROW();
 	}
+	PG_END_TRY();
+
 
 	/*
 	 * now create an entry in pg_class for the relation.
@@ -1308,7 +1340,7 @@ heap_create_with_catalog(const char *relname,
 
 		recordDependencyOnOwner(RelationRelationId, relid, ownerid);
 
-		if (relpersistence != RELPERSISTENCE_TEMP)
+		if (relpersistence != RELPERSISTENCE_TEMP && relpersistence != RELPERSISTENCE_FAST_TEMP)
 			recordDependencyOnCurrentExtension(&myself, false);
 
 		if (reloftypeid)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 7b30e46..d113034 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -30,6 +30,7 @@
 #include "access/transam.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
+#include "access/fasttab.h"
 #include "bootstrap/bootstrap.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
@@ -2284,6 +2285,10 @@ IndexBuildHeapRangeScan(Relation heapRelation,
 	{
 		bool		tupleIsAlive;
 
+		/* ignore in-memory tuples here */
+		if (IsFasttabItemPointer(&heapTuple->t_self))
+			continue;
+
 		CHECK_FOR_INTERRUPTS();
 
 		/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 8fd4c31..47dc2aa 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -284,7 +284,7 @@ RangeVarGetRelidExtended(const RangeVar *relation, LOCKMODE lockmode,
 		 * operation, which must be careful to find the temp table, even when
 		 * pg_temp is not first in the search path.
 		 */
-		if (relation->relpersistence == RELPERSISTENCE_TEMP)
+		if (relation->relpersistence == RELPERSISTENCE_TEMP || relation->relpersistence == RELPERSISTENCE_FAST_TEMP)
 		{
 			if (!OidIsValid(myTempNamespace))
 				relId = InvalidOid;		/* this probably can't happen? */
@@ -463,7 +463,7 @@ RangeVarGetCreationNamespace(const RangeVar *newRelation)
 		namespaceId = get_namespace_oid(newRelation->schemaname, false);
 		/* we do not check for USAGE rights here! */
 	}
-	else if (newRelation->relpersistence == RELPERSISTENCE_TEMP)
+	else if (newRelation->relpersistence == RELPERSISTENCE_TEMP || newRelation->relpersistence == RELPERSISTENCE_FAST_TEMP)
 	{
 		/* Initialize temp namespace if first time through */
 		if (!OidIsValid(myTempNamespace))
@@ -631,6 +631,7 @@ RangeVarAdjustRelationPersistence(RangeVar *newRelation, Oid nspid)
 	switch (newRelation->relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			if (!isTempOrTempToastNamespace(nspid))
 			{
 				if (isAnyTempNamespace(nspid))
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index 0d8311c..99bcf5b 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -85,6 +85,7 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
 	switch (relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			backend = BackendIdForTempRelations();
 			needs_wal = false;
 			break;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 43bbd90..2036ece 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -25,6 +25,7 @@
 #include "access/tuptoaster.h"
 #include "access/xact.h"
 #include "access/xlog.h"
+#include "access/fasttab.h"
 #include "catalog/pg_am.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
@@ -641,7 +642,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, char relpersistence,
 	if (isNull)
 		reloptions = (Datum) 0;
 
-	if (relpersistence == RELPERSISTENCE_TEMP)
+	if (relpersistence == RELPERSISTENCE_TEMP || relpersistence == RELPERSISTENCE_FAST_TEMP)
 		namespaceid = LookupCreationNamespace("pg_temp");
 	else
 		namespaceid = RelationGetNamespace(OldHeap);
@@ -952,6 +953,10 @@ copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 			if (tuple == NULL)
 				break;
 
+			/* No need to move in-memory tuple anywhere */
+			if (IsFasttabItemPointer(&tuple->t_self))
+				continue;
+
 			/* Since we used no scan keys, should never need to recheck */
 			if (indexScan->xs_recheck)
 				elog(ERROR, "CLUSTER does not support lossy index conditions");
@@ -964,6 +969,10 @@ copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 			if (tuple == NULL)
 				break;
 
+			/* No need to move in-memory tuple anywhere */
+			if (IsFasttabItemPointer(&tuple->t_self))
+				continue;
+
 			buf = heapScan->rs_cbuf;
 		}
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index d14d540..49a0ab2 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1945,7 +1945,7 @@ ReindexMultipleTables(const char *objectName, ReindexObjectType objectKind,
 			continue;
 
 		/* Skip temp tables of other backends; we can't reindex them at all */
-		if (classtuple->relpersistence == RELPERSISTENCE_TEMP &&
+		if ((classtuple->relpersistence == RELPERSISTENCE_TEMP || classtuple->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
 			!isTempNamespace(classtuple->relnamespace))
 			continue;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 86e9814..f5cd689 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -22,6 +22,7 @@
 #include "access/sysattr.h"
 #include "access/xact.h"
 #include "access/xlog.h"
+#include "access/fasttab.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
 #include "catalog/heap.h"
@@ -487,7 +488,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	 * Check consistency of arguments
 	 */
 	if (stmt->oncommit != ONCOMMIT_NOOP
-		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP
+		&& stmt->relation->relpersistence != RELPERSISTENCE_FAST_TEMP)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 				 errmsg("ON COMMIT can only be used on temporary tables")));
@@ -506,7 +508,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	 * code.  This is needed because calling code might not expect untrusted
 	 * tables to appear in pg_temp at the front of its search path.
 	 */
-	if (stmt->relation->relpersistence == RELPERSISTENCE_TEMP
+	if ((stmt->relation->relpersistence == RELPERSISTENCE_TEMP ||
+		 stmt->relation->relpersistence == RELPERSISTENCE_FAST_TEMP)
 		&& InSecurityRestrictedOperation())
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1529,14 +1532,17 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 							parent->relname)));
 		/* Permanent rels cannot inherit from temporary ones */
 		if (relpersistence != RELPERSISTENCE_TEMP &&
-			relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+			relpersistence != RELPERSISTENCE_FAST_TEMP &&
+			(relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+			 relation->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP))
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot inherit from temporary relation \"%s\"",
 							parent->relname)));
 
 		/* If existing rel is temp, it must belong to this session */
-		if (relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+		if ((relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+			 relation->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
 			!relation->rd_islocaltemp)
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -6303,7 +6309,9 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
 						 errmsg("constraints on unlogged tables may reference only permanent or unlogged tables")));
 			break;
 		case RELPERSISTENCE_TEMP:
-			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
+		case RELPERSISTENCE_FAST_TEMP:
+			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+				pkrel->rd_rel->relpersistence != RELPERSISTENCE_FAST_TEMP)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 						 errmsg("constraints on temporary tables may reference only temporary tables")));
@@ -10046,22 +10054,26 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode)
 	ATSimplePermissions(parent_rel, ATT_TABLE | ATT_FOREIGN_TABLE);
 
 	/* Permanent rels cannot inherit from temporary ones */
-	if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
-		child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
+	if ((parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		 parent_rel->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
+		(child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+		 child_rel->rd_rel->relpersistence != RELPERSISTENCE_FAST_TEMP))
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("cannot inherit from temporary relation \"%s\"",
 						RelationGetRelationName(parent_rel))));
 
 	/* If parent rel is temp, it must belong to this session */
-	if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+	if ((parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		 parent_rel->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
 		!parent_rel->rd_islocaltemp)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 		errmsg("cannot inherit from temporary relation of another session")));
 
 	/* Ditto for the child */
-	if (child_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+	if ((child_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		 child_rel->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
 		!child_rel->rd_islocaltemp)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -11264,6 +11276,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	switch (rel->rd_rel->relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 					 errmsg("cannot change logged status of table \"%s\" because it is temporary",
diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c
index 7902d43..28c4603 100644
--- a/src/backend/commands/tablespace.c
+++ b/src/backend/commands/tablespace.c
@@ -1117,7 +1117,7 @@ GetDefaultTablespace(char relpersistence)
 	Oid			result;
 
 	/* The temp-table case is handled elsewhere */
-	if (relpersistence == RELPERSISTENCE_TEMP)
+	if (relpersistence == RELPERSISTENCE_TEMP || relpersistence == RELPERSISTENCE_FAST_TEMP)
 	{
 		PrepareTempTablespaces();
 		return GetNextTempTableSpace();
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index 449aacb..3e2cee8 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -37,6 +37,7 @@
 
 #include "access/relscan.h"
 #include "access/transam.h"
+#include "access/fasttab.h"
 #include "executor/execdebug.h"
 #include "executor/nodeBitmapHeapscan.h"
 #include "pgstat.h"
@@ -153,28 +154,36 @@ BitmapHeapNext(BitmapHeapScanState *node)
 			}
 #endif   /* USE_PREFETCH */
 
-			/*
-			 * Ignore any claimed entries past what we think is the end of the
-			 * relation.  (This is probably not necessary given that we got at
-			 * least AccessShareLock on the table before performing any of the
-			 * indexscans, but let's be safe.)
-			 */
-			if (tbmres->blockno >= scan->rs_nblocks)
-			{
-				node->tbmres = tbmres = NULL;
-				continue;
-			}
-
-			/*
-			 * Fetch the current heap page and identify candidate tuples.
-			 */
-			bitgetpage(scan, tbmres);
-
 			if (tbmres->ntuples >= 0)
 				node->exact_pages++;
 			else
 				node->lossy_pages++;
 
+			if(tbmres->blockno < scan->rs_nblocks)
+			{
+				/*
+				 * Normal case. Fetch the current heap page and identify
+				 * candidate tuples.
+                 */
+				bitgetpage(scan, tbmres);
+			}
+			else
+			{
+				/*
+                 * Probably we are looking for an in-memory tuple. This code
+                 * executes in cases when CurrentFasttabBlockId is larger than
+                 * normal block id's.
+                 */
+				OffsetNumber i;
+
+				/* Check all tuples on our virtual page. NB: 0 is an invalid offset */
+				for(i = 1; i <= MaxHeapTuplesPerPage; i++)
+					scan->rs_vistuples[i-1] = FASTTAB_ITEM_POINTER_BIT | i;
+
+				scan->rs_ntuples = MaxHeapTuplesPerPage;
+				tbmres->recheck = true;
+			}
+
 			/*
 			 * Set rs_cindex to first slot to examine
 			 */
@@ -257,15 +266,24 @@ BitmapHeapNext(BitmapHeapScanState *node)
 		 * Okay to fetch the tuple
 		 */
 		targoffset = scan->rs_vistuples[scan->rs_cindex];
-		dp = (Page) BufferGetPage(scan->rs_cbuf);
-		lp = PageGetItemId(dp, targoffset);
-		Assert(ItemIdIsNormal(lp));
-
-		scan->rs_ctup.t_data = (HeapTupleHeader) PageGetItem((Page) dp, lp);
-		scan->rs_ctup.t_len = ItemIdGetLength(lp);
-		scan->rs_ctup.t_tableOid = scan->rs_rd->rd_id;
 		ItemPointerSet(&scan->rs_ctup.t_self, tbmres->blockno, targoffset);
 
+		if (IsFasttabItemPointer(&scan->rs_ctup.t_self))
+		{
+			if(!fasttab_simple_heap_fetch(scan->rs_rd, scan->rs_snapshot, &scan->rs_ctup))
+				continue;
+		}
+		else
+		{
+			dp = (Page) BufferGetPage(scan->rs_cbuf);
+			lp = PageGetItemId(dp, targoffset);
+			Assert(ItemIdIsNormal(lp));
+
+			scan->rs_ctup.t_data = (HeapTupleHeader) PageGetItem((Page) dp, lp);
+			scan->rs_ctup.t_len = ItemIdGetLength(lp);
+			scan->rs_ctup.t_tableOid = scan->rs_rd->rd_id;
+		}
+
 		pgstat_count_heap_fetch(scan->rs_rd);
 
 		/*
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index dfeb7d5..8ede443 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -41,17 +41,19 @@
 #include <limits.h>
 
 #include "access/htup_details.h"
+#include "access/fasttab.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
 #include "utils/hsearch.h"
 
 /*
  * The maximum number of tuples per page is not large (typically 256 with
- * 8K pages, or 1024 with 32K pages).  So there's not much point in making
- * the per-page bitmaps variable size.  We just legislate that the size
+ * 8K pages, or 1024 with 32K pages).  Also in-memory tuples have large fake
+ * offsets because of FASTTAB_ITEM_POINTER_BIT. So there's not much point in
+ * making the per-page bitmaps variable size.  We just legislate that the size
  * is this:
  */
-#define MAX_TUPLES_PER_PAGE  MaxHeapTuplesPerPage
+#define MAX_TUPLES_PER_PAGE (FASTTAB_ITEM_POINTER_BIT | MaxHeapTuplesPerPage)
 
 /*
  * When we have to switch over to lossy storage, we use a data structure
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 88d833a..976ca06 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -501,6 +501,8 @@ static void
 set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 						  RangeTblEntry *rte)
 {
+	char relpersistence;
+
 	/*
 	 * The flag has previously been initialized to false, so we can just
 	 * return if it becomes clear that we can't safely set it.
@@ -529,7 +531,8 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 			 * the rest of the necessary infrastructure right now anyway.  So
 			 * for now, bail out if we see a temporary table.
 			 */
-			if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP)
+			relpersistence = get_rel_persistence(rte->relid);
+			if (relpersistence == RELPERSISTENCE_TEMP || relpersistence == RELPERSISTENCE_FAST_TEMP)
 				return;
 
 			/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0cae446..e5220d2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -586,7 +586,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
 	EXTENSION EXTERNAL EXTRACT
 
-	FALSE_P FAMILY FETCH FILTER FIRST_P FLOAT_P FOLLOWING FOR
+	FALSE_P FAMILY FAST FETCH FILTER FIRST_P FLOAT_P FOLLOWING FOR
 	FORCE FOREIGN FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
 
 	GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING
@@ -2891,6 +2891,8 @@ OptTemp:	TEMPORARY					{ $$ = RELPERSISTENCE_TEMP; }
 			| TEMP						{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMPORARY			{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMP				{ $$ = RELPERSISTENCE_TEMP; }
+			| FAST TEMPORARY			{ $$ = RELPERSISTENCE_FAST_TEMP; }
+			| FAST TEMP				{ $$ = RELPERSISTENCE_FAST_TEMP; }
 			| GLOBAL TEMPORARY
 				{
 					ereport(WARNING,
@@ -10280,6 +10282,16 @@ OptTempTableName:
 					$$ = $4;
 					$$->relpersistence = RELPERSISTENCE_TEMP;
 				}
+			| FAST TEMPORARY opt_table qualified_name
+				{
+					$$ = $4;
+					$$->relpersistence = RELPERSISTENCE_FAST_TEMP;
+				}
+			| FAST TEMP opt_table qualified_name
+				{
+					$$ = $4;
+					$$->relpersistence = RELPERSISTENCE_FAST_TEMP;
+				}
 			| GLOBAL TEMPORARY opt_table qualified_name
 				{
 					ereport(WARNING,
@@ -13807,6 +13819,7 @@ unreserved_keyword:
 			| EXTENSION
 			| EXTERNAL
 			| FAMILY
+			| FAST
 			| FILTER
 			| FIRST_P
 			| FOLLOWING
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 1e3ecbc..1ed569d 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -3141,7 +3141,7 @@ isQueryUsingTempRelation_walker(Node *node, void *context)
 				char		relpersistence = rel->rd_rel->relpersistence;
 
 				heap_close(rel, AccessShareLock);
-				if (relpersistence == RELPERSISTENCE_TEMP)
+				if (relpersistence == RELPERSISTENCE_TEMP || relpersistence == RELPERSISTENCE_FAST_TEMP)
 					return true;
 			}
 		}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e98fad0..7f71706 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -202,7 +202,8 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 	 * specified to be in pg_temp, so no need for anything extra in that case.
 	 */
 	if (stmt->relation->schemaname == NULL
-		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP
+		&& stmt->relation->relpersistence != RELPERSISTENCE_FAST_TEMP)
 		stmt->relation->schemaname = get_namespace_name(namespaceid);
 
 	/* Set up CreateStmtContext */
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 3768f50..4fe00ee 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2037,7 +2037,8 @@ do_autovacuum(void)
 		 * Check if it is a temp table (presumably, of some other backend's).
 		 * We cannot safely process other backends' temp tables.
 		 */
-		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+		if (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+			classForm->relpersistence == RELPERSISTENCE_FAST_TEMP)
 		{
 			int			backendID;
 
@@ -2134,7 +2135,8 @@ do_autovacuum(void)
 		/*
 		 * We cannot safely process other backends' temp tables, so skip 'em.
 		 */
-		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+		if (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+			classForm->relpersistence == RELPERSISTENCE_FAST_TEMP)
 			continue;
 
 		relid = HeapTupleGetOid(tuple);
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 0776f3b..a0ebbad 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -1003,6 +1003,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 			backend = InvalidBackendId;
 			break;
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			if (isTempOrTempToastNamespace(relform->relnamespace))
 				backend = BackendIdForTempRelations();
 			else
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8d2ad01..f811afe 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -991,6 +991,7 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 			relation->rd_islocaltemp = false;
 			break;
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			if (isTempOrTempToastNamespace(relation->rd_rel->relnamespace))
 			{
 				relation->rd_backend = BackendIdForTempRelations();
@@ -1937,6 +1938,7 @@ RelationReloadIndexInfo(Relation relation)
 			 RelationGetRelid(relation));
 	relp = (Form_pg_class) GETSTRUCT(pg_class_tuple);
 	memcpy(relation->rd_rel, relp, CLASS_TUPLE_SIZE);
+
 	/* Reload reloptions in case they changed */
 	if (relation->rd_options)
 		pfree(relation->rd_options);
@@ -2974,6 +2976,7 @@ RelationBuildLocalRelation(const char *relname,
 			rel->rd_islocaltemp = false;
 			break;
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			Assert(isTempOrTempToastNamespace(relnamespace));
 			rel->rd_backend = BackendIdForTempRelations();
 			rel->rd_islocaltemp = true;
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8469d9f..d3ccac4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -894,6 +894,7 @@ static const pgsql_thing_t words_after_create[] = {
 	{"DOMAIN", NULL, &Query_for_list_of_domains},
 	{"EVENT TRIGGER", NULL, NULL},
 	{"EXTENSION", Query_for_list_of_extensions},
+	{"FAST TEMP", NULL, NULL, THING_NO_DROP},		/* for CREATE FAST TEMP TABLE ... */
 	{"FOREIGN DATA WRAPPER", NULL, NULL},
 	{"FOREIGN TABLE", NULL, NULL},
 	{"FUNCTION", NULL, &Query_for_list_of_functions},
diff --git a/src/include/access/fasttab.h b/src/include/access/fasttab.h
new file mode 100644
index 0000000..2d38872
--- /dev/null
+++ b/src/include/access/fasttab.h
@@ -0,0 +1,83 @@
+/* FOR INTERNAL USAGE ONLY. Backward compatability is not guaranteed, dont use in extensions! */
+
+#ifndef FASTTAB_H
+#define FASTTAB_H
+
+#include "c.h"
+#include "postgres_ext.h"
+#include "access/htup.h"
+#include "access/heapam.h"
+#include "access/sdir.h"
+#include "access/genam.h"
+#include "catalog/indexing.h"
+#include "storage/itemptr.h"
+#include "utils/relcache.h"
+
+/*
+ * ItemPointerData.ip_posid is _never_ that large. See MaxHeapTuplesPerPage constant.
+ * This constant better be not too large since MAX_TUPLES_PER_PAGE depends on it value.
+ */
+#define FASTTAB_ITEM_POINTER_BIT 0x0800
+
+#define IsFasttabItemPointer(ptr) \
+	( ((ptr)->ip_posid & FASTTAB_ITEM_POINTER_BIT) != 0 )
+
+typedef struct FasttabIndexMethodsData FasttabIndexMethodsData;
+
+typedef FasttabIndexMethodsData const *FasttabIndexMethods;
+
+extern bool IsFasttabHandledRelationId(Oid relId);
+
+extern bool IsFasttabHandledIndexId(Oid indexId);
+
+extern void fasttab_set_relpersistence_hint(char relpersistence);
+
+extern void fasttab_clear_relpersistence_hint(void);
+
+extern void fasttab_begin_transaction(void);
+
+extern void fasttab_end_transaction(void);
+
+extern void fasttab_abort_transaction(void);
+
+extern void fasttab_define_savepoint(const char *name);
+
+extern void fasttab_rollback_to_savepoint(const char *name);
+
+extern void fasttab_beginscan(HeapScanDesc scan);
+
+extern HeapTuple fasttab_getnext(HeapScanDesc scan, ScanDirection direction);
+
+extern bool fasttab_hot_search_buffer(ItemPointer tid, Relation relation,
+						  HeapTuple heapTuple, bool *all_dead, bool *result);
+
+extern bool fasttab_insert(Relation relation, HeapTuple tup, HeapTuple heaptup,
+			   Oid *result);
+
+extern bool fasttab_delete(Relation relation, ItemPointer tid);
+
+extern bool fasttab_update(Relation relation, ItemPointer otid,
+			   HeapTuple newtup);
+
+extern bool fasttab_inplace_update(Relation relation, HeapTuple tuple);
+
+extern bool fasttab_index_insert(Relation indexRelation,
+					 ItemPointer heap_t_ctid, bool *result);
+
+extern void fasttab_index_beginscan(IndexScanDesc scan);
+
+extern void fasttab_index_rescan(IndexScanDesc scan, ScanKey keys, int nkeys,
+					 ScanKey orderbys, int norderbys);
+
+extern bool fasttab_simple_heap_fetch(Relation relation, Snapshot snapshot,
+						  HeapTuple tuple);
+
+extern bool fasttab_index_getnext_tid_merge(IndexScanDesc scan,
+								ScanDirection direction);
+
+extern bool fasttab_index_getbitmap(IndexScanDesc scan, TIDBitmap *bitmap,
+						int64 *result);
+
+extern void fasttab_index_endscan(IndexScanDesc scan);
+
+#endif   /* FASTTAB_H */
diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 49c2a6f..456a7ff 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -19,6 +19,7 @@
 #include "access/htup_details.h"
 #include "access/itup.h"
 #include "access/tupdesc.h"
+#include "access/fasttab.h"
 
 /*
  * Shared state for parallel heap scan.
@@ -74,6 +75,8 @@ typedef struct HeapScanDescData
 	int			rs_cindex;		/* current tuple's index in vistuples */
 	int			rs_ntuples;		/* number of visible tuples on page */
 	OffsetNumber rs_vistuples[MaxHeapTuplesPerPage];	/* their offsets */
+
+	dlist_node *rs_curr_inmem_tupnode;	/* current in-memory tuple, or NULL */
 }	HeapScanDescData;
 
 /*
@@ -125,6 +128,13 @@ typedef struct IndexScanDescData
 
 	/* state data for traversing HOT chains in index_getnext */
 	bool		xs_continue_hot;	/* T if must keep walking HOT chain */
+
+	FasttabIndexMethods indexMethods;
+	dlist_head	xs_inmem_tuplist;
+	bool		xs_regular_tuple_enqueued;
+	bool		xs_regular_scan_finished;
+	bool		xs_scan_finish_returned;
+	bool		xs_inmem_tuplist_init_done;
 }	IndexScanDescData;
 
 /* Struct for heap-or-index scans of system tables */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index e57b81c..f51a91a 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -162,9 +162,11 @@ DESCR("");
 #define		  RELKIND_FOREIGN_TABLE   'f'		/* foreign table */
 #define		  RELKIND_MATVIEW		  'm'		/* materialized view */
 
+#define		  RELPERSISTENCE_UNDEFINED  '?'		/* invalid relpersistence value */
 #define		  RELPERSISTENCE_PERMANENT	'p'		/* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u'		/* unlogged permanent table */
 #define		  RELPERSISTENCE_TEMP		't'		/* temporary table */
+#define		  RELPERSISTENCE_FAST_TEMP	'f'		/* fast temporary table */
 
 /* default selection for replica identity (primary key or nothing) */
 #define		  REPLICA_IDENTITY_DEFAULT	'd'
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 17ffef5..a673176 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -157,6 +157,7 @@ PG_KEYWORD("external", EXTERNAL, UNRESERVED_KEYWORD)
 PG_KEYWORD("extract", EXTRACT, COL_NAME_KEYWORD)
 PG_KEYWORD("false", FALSE_P, RESERVED_KEYWORD)
 PG_KEYWORD("family", FAMILY, UNRESERVED_KEYWORD)
+PG_KEYWORD("fast", FAST, UNRESERVED_KEYWORD)
 PG_KEYWORD("fetch", FETCH, RESERVED_KEYWORD)
 PG_KEYWORD("filter", FILTER, UNRESERVED_KEYWORD)
 PG_KEYWORD("first", FIRST_P, UNRESERVED_KEYWORD)
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index ed14442..543fad0 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -463,9 +463,11 @@ typedef struct ViewOptions
 /*
  * RelationUsesLocalBuffers
  *		True if relation's pages are stored in local buffers.
+ *
+ * Beware of multiple eval of argument
  */
 #define RelationUsesLocalBuffers(relation) \
-	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	(((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP) || ((relation)->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP))
 
 /*
  * RELATION_IS_LOCAL
@@ -486,7 +488,7 @@ typedef struct ViewOptions
  * Beware of multiple eval of argument
  */
 #define RELATION_IS_OTHER_TEMP(relation) \
-	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP && \
+	((((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP) || ((relation)->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP)) && \
 	 !(relation)->rd_islocaltemp)
 
 
diff --git a/src/test/regress/expected/fast_temp.out b/src/test/regress/expected/fast_temp.out
new file mode 100644
index 0000000..bbd741d
--- /dev/null
+++ b/src/test/regress/expected/fast_temp.out
@@ -0,0 +1,385 @@
+--
+-- FAST TEMP
+-- Test fast temporary tables
+--
+-- basic test
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+INSERT INTO fasttab_test1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'), (4, 'ddd');
+UPDATE fasttab_test1 SET s = 'eee' WHERE x = 4;
+UPDATE fasttab_test1 SET x = 5 WHERE s = 'bbb';
+DELETE FROM fasttab_test1 WHERE x = 3;
+SELECT * FROM fasttab_test1 ORDER BY x;
+ x |  s  
+---+-----
+ 1 | aaa
+ 4 | eee
+ 5 | bbb
+(3 rows)
+
+DROP TABLE fasttab_test1;
+-- kind of load test
+do $$
+declare
+  count_fast_table integer = 150;
+  count_attr integer = 20;
+  i integer;
+  j integer;
+  t_sql text;
+begin
+  for i in 1 .. count_fast_table
+  loop
+    t_sql = 'CREATE FAST TEMP TABLE fast_table_' || i :: text;
+    t_sql = t_sql || '  (';
+    for j in 1 .. count_attr
+    loop
+      t_sql = t_sql || ' attr' || j || ' text';
+      if j <> count_attr then
+        t_sql = t_sql || ', ';
+      end if;
+    end loop;
+    t_sql = t_sql || ' );';
+    execute t_sql;
+    -- raise info 't_sql %', t_sql;
+  end loop;
+end $$;
+SELECT * FROM fast_table_1;
+ attr1 | attr2 | attr3 | attr4 | attr5 | attr6 | attr7 | attr8 | attr9 | attr10 | attr11 | attr12 | attr13 | attr14 | attr15 | attr16 | attr17 | attr18 | attr19 | attr20 
+-------+-------+-------+-------+-------+-------+-------+-------+-------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------
+(0 rows)
+
+-- test bitmap index scan
+SELECT count(*) FROM pg_class WHERE relname = 'fast_table_1' OR relname = 'fast_table_2';
+ count 
+-------
+     2
+(1 row)
+
+-- create / delete / create test
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+-- check index only scan
+SELECT COUNT(*) FROM pg_class WHERE relname = 'fasttab_test1';
+ count 
+-------
+     1
+(1 row)
+
+SELECT relname FROM pg_class WHERE relname = 'fasttab_test1';
+    relname    
+---------------
+ fasttab_test1
+(1 row)
+
+DROP TABLE fasttab_test1;
+-- select from non-existend temp table
+SELECT COUNT(*) FROM fasttab_test1;
+ERROR:  relation "fasttab_test1" does not exist
+LINE 1: SELECT COUNT(*) FROM fasttab_test1;
+                             ^
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+CREATE FAST TEMP TABLE fasttab_test2(x int, s text);
+SELECT * FROM fasttab_test1;
+ x | s 
+---+---
+(0 rows)
+
+-- check that ALTER is working as expected
+ALTER TABLE fasttab_test1 ADD COLUMN y int;
+SELECT * FROM fasttab_test1;
+ x | s | y 
+---+---+---
+(0 rows)
+
+ALTER TABLE fasttab_test1 ADD COLUMN z int;
+SELECT * FROM fasttab_test1;
+ x | s | y | z 
+---+---+---+---
+(0 rows)
+
+ALTER TABLE fasttab_test1 DROP COLUMN x;
+SELECT * FROM fasttab_test1;
+ s | y | z 
+---+---+---
+(0 rows)
+
+ALTER TABLE fasttab_test1 DROP COLUMN y;
+SELECT * FROM fasttab_test1;
+ s | z 
+---+---
+(0 rows)
+
+-- test transactions and savepoints
+BEGIN;
+INSERT INTO fasttab_test2 VALUES (1, 'aaa'), (2, 'bbb');
+SELECT * FROM fasttab_test2;
+ x |  s  
+---+-----
+ 1 | aaa
+ 2 | bbb
+(2 rows)
+
+ROLLBACK;
+SELECT * FROM fasttab_test2;
+ x | s 
+---+---
+(0 rows)
+
+BEGIN;
+INSERT INTO fasttab_test2 VALUES (3, 'ccc'), (4, 'ddd');
+SELECT * FROM fasttab_test2;
+ x |  s  
+---+-----
+ 3 | ccc
+ 4 | ddd
+(2 rows)
+
+COMMIT;
+SELECT * FROM fasttab_test2;
+ x |  s  
+---+-----
+ 3 | ccc
+ 4 | ddd
+(2 rows)
+
+BEGIN;
+SAVEPOINT sp1;
+ALTER TABLE fasttab_test2 ADD COLUMN y int;
+SELECT * FROM fasttab_test2;
+ x |  s  | y 
+---+-----+---
+ 3 | ccc |  
+ 4 | ddd |  
+(2 rows)
+
+SAVEPOINT sp2;
+INSERT INTO fasttab_test2 VALUES (5, 'eee', 6);
+SELECT * FROM fasttab_test2;
+ x |  s  | y 
+---+-----+---
+ 3 | ccc |  
+ 4 | ddd |  
+ 5 | eee | 6
+(3 rows)
+
+ROLLBACK TO SAVEPOINT sp2;
+INSERT INTO fasttab_test2 VALUES (55, 'EEE', 66);
+SELECT * FROM fasttab_test2;
+ x  |  s  | y  
+----+-----+----
+  3 | ccc |   
+  4 | ddd |   
+ 55 | EEE | 66
+(3 rows)
+
+ROLLBACK TO SAVEPOINT sp2;
+SELECT * FROM fasttab_test2;
+ x |  s  | y 
+---+-----+---
+ 3 | ccc |  
+ 4 | ddd |  
+(2 rows)
+
+COMMIT;
+-- test that exceptions are handled properly
+DO $$
+DECLARE
+BEGIN
+    CREATE FAST TEMP TABLE fast_exception_test(x int, y int, z int);
+    RAISE EXCEPTION 'test error';
+END $$;
+ERROR:  test error
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at RAISE
+CREATE FAST TEMP TABLE fast_exception_test(x int, y int, z int);
+-- test that inheritance works as expected
+-- OK:
+CREATE TABLE cities (name text, population float, altitude int);
+CREATE TABLE capitals (state char(2)) INHERITS (cities);
+DROP TABLE capitals;
+DROP TABLE cities;
+-- OK:
+CREATE TABLE cities2 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals2 (state char(2)) INHERITS (cities2);
+INSERT INTO capitals2 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals2 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals2;
+  name  | population | altitude | state 
+--------+------------+----------+-------
+ Moscow |     123.45 |      789 | RU
+ Paris  |     543.21 |      987 | FR
+(2 rows)
+
+SELECT * FROM cities2;
+  name  | population | altitude 
+--------+------------+----------
+ Moscow |     123.45 |      789
+ Paris  |     543.21 |      987
+(2 rows)
+
+DELETE FROM cities2 WHERE name = 'Moscow';
+SELECT * FROM capitals2;
+ name  | population | altitude | state 
+-------+------------+----------+-------
+ Paris |     543.21 |      987 | FR
+(1 row)
+
+SELECT * FROM cities2;
+ name  | population | altitude 
+-------+------------+----------
+ Paris |     543.21 |      987
+(1 row)
+
+DROP TABLE capitals2;
+DROP TABLE cities2;
+-- ERROR:
+CREATE FAST TEMPORARY TABLE cities3 (name text, population float, altitude int);
+-- cannot inherit from temporary relation "cities3"
+CREATE TABLE capitals3 (state char(2)) INHERITS (cities3);
+ERROR:  cannot inherit from temporary relation "cities3"
+DROP TABLE cities3;
+-- OK:
+CREATE FAST TEMPORARY TABLE cities4 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals4 (state char(2)) INHERITS (cities4);
+INSERT INTO capitals4 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals4 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals4;
+  name  | population | altitude | state 
+--------+------------+----------+-------
+ Moscow |     123.45 |      789 | RU
+ Paris  |     543.21 |      987 | FR
+(2 rows)
+
+SELECT * FROM cities4;
+  name  | population | altitude 
+--------+------------+----------
+ Moscow |     123.45 |      789
+ Paris  |     543.21 |      987
+(2 rows)
+
+DELETE FROM cities4 WHERE name = 'Moscow';
+SELECT * FROM capitals4;
+ name  | population | altitude | state 
+-------+------------+----------+-------
+ Paris |     543.21 |      987 | FR
+(1 row)
+
+SELECT * FROM cities4;
+ name  | population | altitude 
+-------+------------+----------
+ Paris |     543.21 |      987
+(1 row)
+
+DROP TABLE capitals4;
+DROP TABLE cities4;
+-- OK:
+CREATE TEMPORARY TABLE cities5 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals5 (state char(2)) INHERITS (cities5);
+INSERT INTO capitals5 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals5 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals5;
+  name  | population | altitude | state 
+--------+------------+----------+-------
+ Moscow |     123.45 |      789 | RU
+ Paris  |     543.21 |      987 | FR
+(2 rows)
+
+SELECT * FROM cities5;
+  name  | population | altitude 
+--------+------------+----------
+ Moscow |     123.45 |      789
+ Paris  |     543.21 |      987
+(2 rows)
+
+DELETE FROM cities5 WHERE name = 'Moscow';
+SELECT * FROM capitals5;
+ name  | population | altitude | state 
+-------+------------+----------+-------
+ Paris |     543.21 |      987 | FR
+(1 row)
+
+SELECT * FROM cities5;
+ name  | population | altitude 
+-------+------------+----------
+ Paris |     543.21 |      987
+(1 row)
+
+DROP TABLE capitals5;
+DROP TABLE cities5;
+-- OK:
+CREATE FAST TEMPORARY TABLE cities6 (name text, population float, altitude int);
+CREATE TEMPORARY TABLE capitals6 (state char(2)) INHERITS (cities6);
+INSERT INTO capitals6 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals6 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals6;
+  name  | population | altitude | state 
+--------+------------+----------+-------
+ Moscow |     123.45 |      789 | RU
+ Paris  |     543.21 |      987 | FR
+(2 rows)
+
+SELECT * FROM cities6;
+  name  | population | altitude 
+--------+------------+----------
+ Moscow |     123.45 |      789
+ Paris  |     543.21 |      987
+(2 rows)
+
+DELETE FROM cities6 WHERE name = 'Moscow';
+SELECT * FROM capitals6;
+ name  | population | altitude | state 
+-------+------------+----------+-------
+ Paris |     543.21 |      987 | FR
+(1 row)
+
+SELECT * FROM cities6;
+ name  | population | altitude 
+-------+------------+----------
+ Paris |     543.21 |      987
+(1 row)
+
+DROP TABLE capitals6;
+DROP TABLE cities6;
+-- test index-only scan
+CREATE FAST TEMP TABLE fasttab_unique_prefix_beta(x int);
+CREATE TABLE fasttab_unique_prefix_alpha(x int);
+CREATE FAST TEMP TABLE fasttab_unique_prefix_delta(x int);
+CREATE TABLE fasttab_unique_prefix_epsilon(x int);
+CREATE TABLE fasttab_unique_prefix_gamma(x int);
+SELECT relname FROM pg_class WHERE relname > 'fasttab_unique_prefix_' ORDER BY relname LIMIT 5;
+            relname            
+-------------------------------
+ fasttab_unique_prefix_alpha
+ fasttab_unique_prefix_beta
+ fasttab_unique_prefix_delta
+ fasttab_unique_prefix_epsilon
+ fasttab_unique_prefix_gamma
+(5 rows)
+
+DROP TABLE fasttab_unique_prefix_alpha;
+DROP TABLE fasttab_unique_prefix_beta;
+DROP TABLE fasttab_unique_prefix_gamma;
+DROP TABLE fasttab_unique_prefix_delta;
+DROP TABLE fasttab_unique_prefix_epsilon;
+-- test VACUUM / VACUUM FULL
+VACUUM;
+VACUUM FULL;
+SELECT * FROM fast_table_1;
+ attr1 | attr2 | attr3 | attr4 | attr5 | attr6 | attr7 | attr8 | attr9 | attr10 | attr11 | attr12 | attr13 | attr14 | attr15 | attr16 | attr17 | attr18 | attr19 | attr20 
+-------+-------+-------+-------+-------+-------+-------+-------+-------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------
+(0 rows)
+
+-- test ANALYZE
+CREATE FAST TEMP TABLE fasttab_analyze_test(x int, s text);
+INSERT INTO fasttab_analyze_test SELECT x, '--> ' || x FROM generate_series(1,100) as x;
+ANALYZE fasttab_analyze_test;
+SELECT count(*) FROM pg_statistic WHERE starelid = (SELECT oid FROM pg_class WHERE relname = 'fasttab_analyze_test');
+ count 
+-------
+     2
+(1 row)
+
+DROP TABLE fasttab_analyze_test;
+SELECT count(*) FROM pg_statistic WHERE starelid = (SELECT oid FROM pg_class WHERE relname = 'fasttab_analyze_test');
+ count 
+-------
+     0
+(1 row)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4ebad04..60b9bec 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -105,6 +105,11 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
 
+# ----------
+# Another group of parallel tests
+# ----------
+test: fast_temp
+
 # event triggers cannot run concurrently with any test that runs DDL
 test: event_trigger
 
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 5c7038d..f5aa850 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -151,6 +151,7 @@ test: limit
 test: plpgsql
 test: copy2
 test: temp
+test: fast_temp
 test: domain
 test: rangefuncs
 test: prepare
diff --git a/src/test/regress/sql/fast_temp.sql b/src/test/regress/sql/fast_temp.sql
new file mode 100644
index 0000000..859e895
--- /dev/null
+++ b/src/test/regress/sql/fast_temp.sql
@@ -0,0 +1,238 @@
+--
+-- FAST TEMP
+-- Test fast temporary tables
+--
+
+-- basic test
+
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+
+INSERT INTO fasttab_test1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'), (4, 'ddd');
+
+UPDATE fasttab_test1 SET s = 'eee' WHERE x = 4;
+
+UPDATE fasttab_test1 SET x = 5 WHERE s = 'bbb';
+
+DELETE FROM fasttab_test1 WHERE x = 3;
+
+SELECT * FROM fasttab_test1 ORDER BY x;
+
+DROP TABLE fasttab_test1;
+
+-- kind of load test
+
+do $$
+declare
+  count_fast_table integer = 150;
+  count_attr integer = 20;
+  i integer;
+  j integer;
+  t_sql text;
+begin
+  for i in 1 .. count_fast_table
+  loop
+    t_sql = 'CREATE FAST TEMP TABLE fast_table_' || i :: text;
+    t_sql = t_sql || '  (';
+    for j in 1 .. count_attr
+    loop
+      t_sql = t_sql || ' attr' || j || ' text';
+      if j <> count_attr then
+        t_sql = t_sql || ', ';
+      end if;
+    end loop;
+    t_sql = t_sql || ' );';
+    execute t_sql;
+    -- raise info 't_sql %', t_sql;
+  end loop;
+end $$;
+
+SELECT * FROM fast_table_1;
+
+-- test bitmap index scan
+
+SELECT count(*) FROM pg_class WHERE relname = 'fast_table_1' OR relname = 'fast_table_2';
+
+-- create / delete / create test
+
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+
+-- check index only scan
+
+SELECT COUNT(*) FROM pg_class WHERE relname = 'fasttab_test1';
+SELECT relname FROM pg_class WHERE relname = 'fasttab_test1';
+
+DROP TABLE fasttab_test1;
+
+-- select from non-existend temp table
+
+SELECT COUNT(*) FROM fasttab_test1;
+
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+CREATE FAST TEMP TABLE fasttab_test2(x int, s text);
+SELECT * FROM fasttab_test1;
+
+-- check that ALTER is working as expected
+
+ALTER TABLE fasttab_test1 ADD COLUMN y int;
+SELECT * FROM fasttab_test1;
+
+ALTER TABLE fasttab_test1 ADD COLUMN z int;
+SELECT * FROM fasttab_test1;
+
+ALTER TABLE fasttab_test1 DROP COLUMN x;
+SELECT * FROM fasttab_test1;
+
+ALTER TABLE fasttab_test1 DROP COLUMN y;
+SELECT * FROM fasttab_test1;
+
+-- test transactions and savepoints
+
+BEGIN;
+
+INSERT INTO fasttab_test2 VALUES (1, 'aaa'), (2, 'bbb');
+SELECT * FROM fasttab_test2;
+
+ROLLBACK;
+
+SELECT * FROM fasttab_test2;
+
+BEGIN;
+
+INSERT INTO fasttab_test2 VALUES (3, 'ccc'), (4, 'ddd');
+SELECT * FROM fasttab_test2;
+
+COMMIT;
+
+SELECT * FROM fasttab_test2;
+
+
+BEGIN;
+
+SAVEPOINT sp1;
+
+ALTER TABLE fasttab_test2 ADD COLUMN y int;
+SELECT * FROM fasttab_test2;
+
+SAVEPOINT sp2;
+
+INSERT INTO fasttab_test2 VALUES (5, 'eee', 6);
+SELECT * FROM fasttab_test2;
+ROLLBACK TO SAVEPOINT sp2;
+
+INSERT INTO fasttab_test2 VALUES (55, 'EEE', 66);
+SELECT * FROM fasttab_test2;
+ROLLBACK TO SAVEPOINT sp2;
+
+SELECT * FROM fasttab_test2;
+COMMIT;
+
+-- test that exceptions are handled properly
+
+DO $$
+DECLARE
+BEGIN
+    CREATE FAST TEMP TABLE fast_exception_test(x int, y int, z int);
+    RAISE EXCEPTION 'test error';
+END $$;
+
+CREATE FAST TEMP TABLE fast_exception_test(x int, y int, z int);
+
+-- test that inheritance works as expected
+-- OK:
+
+CREATE TABLE cities (name text, population float, altitude int);
+CREATE TABLE capitals (state char(2)) INHERITS (cities);
+DROP TABLE capitals;
+DROP TABLE cities;
+
+-- OK:
+
+CREATE TABLE cities2 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals2 (state char(2)) INHERITS (cities2);
+INSERT INTO capitals2 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals2 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals2;
+SELECT * FROM cities2;
+DELETE FROM cities2 WHERE name = 'Moscow';
+SELECT * FROM capitals2;
+SELECT * FROM cities2;
+DROP TABLE capitals2;
+DROP TABLE cities2;
+
+-- ERROR:
+
+CREATE FAST TEMPORARY TABLE cities3 (name text, population float, altitude int);
+-- cannot inherit from temporary relation "cities3"
+CREATE TABLE capitals3 (state char(2)) INHERITS (cities3);
+DROP TABLE cities3;
+
+-- OK:
+
+CREATE FAST TEMPORARY TABLE cities4 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals4 (state char(2)) INHERITS (cities4);
+INSERT INTO capitals4 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals4 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals4;
+SELECT * FROM cities4;
+DELETE FROM cities4 WHERE name = 'Moscow';
+SELECT * FROM capitals4;
+SELECT * FROM cities4;
+DROP TABLE capitals4;
+DROP TABLE cities4;
+
+-- OK:
+
+CREATE TEMPORARY TABLE cities5 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals5 (state char(2)) INHERITS (cities5);
+INSERT INTO capitals5 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals5 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals5;
+SELECT * FROM cities5;
+DELETE FROM cities5 WHERE name = 'Moscow';
+SELECT * FROM capitals5;
+SELECT * FROM cities5;
+DROP TABLE capitals5;
+DROP TABLE cities5;
+
+-- OK:
+
+CREATE FAST TEMPORARY TABLE cities6 (name text, population float, altitude int);
+CREATE TEMPORARY TABLE capitals6 (state char(2)) INHERITS (cities6);
+INSERT INTO capitals6 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals6 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals6;
+SELECT * FROM cities6;
+DELETE FROM cities6 WHERE name = 'Moscow';
+SELECT * FROM capitals6;
+SELECT * FROM cities6;
+DROP TABLE capitals6;
+DROP TABLE cities6;
+
+-- test index-only scan
+
+CREATE FAST TEMP TABLE fasttab_unique_prefix_beta(x int);
+CREATE TABLE fasttab_unique_prefix_alpha(x int);
+CREATE FAST TEMP TABLE fasttab_unique_prefix_delta(x int);
+CREATE TABLE fasttab_unique_prefix_epsilon(x int);
+CREATE TABLE fasttab_unique_prefix_gamma(x int);
+SELECT relname FROM pg_class WHERE relname > 'fasttab_unique_prefix_' ORDER BY relname LIMIT 5;
+DROP TABLE fasttab_unique_prefix_alpha;
+DROP TABLE fasttab_unique_prefix_beta;
+DROP TABLE fasttab_unique_prefix_gamma;
+DROP TABLE fasttab_unique_prefix_delta;
+DROP TABLE fasttab_unique_prefix_epsilon;
+
+-- test VACUUM / VACUUM FULL
+
+VACUUM;
+VACUUM FULL;
+SELECT * FROM fast_table_1;
+
+-- test ANALYZE
+
+CREATE FAST TEMP TABLE fasttab_analyze_test(x int, s text);
+INSERT INTO fasttab_analyze_test SELECT x, '--> ' || x FROM generate_series(1,100) as x;
+ANALYZE fasttab_analyze_test;
+SELECT count(*) FROM pg_statistic WHERE starelid = (SELECT oid FROM pg_class WHERE relname = 'fasttab_analyze_test');
+DROP TABLE fasttab_analyze_test;
+SELECT count(*) FROM pg_statistic WHERE starelid = (SELECT oid FROM pg_class WHERE relname = 'fasttab_analyze_test');
#2Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Aleksander Alekseev (#1)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Hi,

On 07/29/2016 01:15 PM, Aleksander Alekseev wrote:

Hello

Some time ago we discussed an idea of "fast temporary tables":

/messages/by-id/20160301182500.2c81c3dc@fujitsu

In two words the idea is following.

<The Idea>

PostgreSQL stores information about all relations in pg_catalog. Some
applications create and delete a lot of temporary tables. It causes a
bloating of pg_catalog and running auto vacuum on it. It's quite an
expensive operation which affects entire database performance.

We could introduce a new type of temporary tables. Information about
these tables is stored not in a catalog but in backend's memory. This
way user can solve a pg_catalog bloating problem and improve overall
database performance.

</The Idea>

Great! Thanks for the patch, this is definitely an annoying issue worth
fixing. I've spent a bit of time looking at the patch today, comments
below ...

I took me a few months but eventually I made it work. Attached patch
has some flaws. I decided not to invest a lot of time in documenting
it or pgindent'ing all files yet. In my experience it will be rewritten
entirely 3 or 4 times before merging anyway :) But it _works_ and
passes all tests I could think of, including non-trivial cases like
index-only or bitmap scans of catalog tables.

Well, jokes aside, that's a pretty lousy excuse for not writing any
docs, and you're pretty much asking the reviewers to reverse-engineer
your reasoning. So I doubt you'll get many serious reviews without
fixing this gap.

Usage example:

```
CREATE FAST TEMP TABLE fasttab_test1(x int, s text);

INSERT INTO fasttab_test1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc');

UPDATE fasttab_test1 SET s = 'ddd' WHERE x = 2;

DELETE FROM fasttab_test1 WHERE x = 3;

SELECT * FROM fasttab_test1 ORDER BY x;

DROP TABLE fasttab_test1;
```

More sophisticated examples could be find in regression tests:

./src/test/regress/sql/fast_temp.sql

Any feedback on this patch will be much appreciated!

1) I wonder whether the FAST makes sense - does this really change the
performance significantly? IMHO you only move the catalog rows to
memory, so why should the tables be any faster? I also believe this
conflicts with SQL standard specification of CREATE TABLE.

2) Why do we need the new relpersistence value? ISTM we could easily got
with just RELPERSISTENCE_TEMP, which would got right away of many
chances as the steps are exactly the same.

IMHO if this patch gets in, we should use it as the only temp table
implementation (Or can you think of cases where keeping rows in pg_class
has advantages?). That'd also eliminate the need for FAST keyword in the
CREATE TABLE command.

The one thin I'm not sure about is that our handling of temporary tables
is not standard compliant - we require each session to create it's own
private temporary table. Moving the rows from pg_class into backend
memory seems to go in the opposite direction, but as no one was planning
to fix this, I don't think it matters much.

3) I think the heapam/indexam/xact and various other places needs a
major rework. You've mostly randomly sprinkled the code with function
calls to make the patch work - that's fine for an initial version, but a
more principled approach is needed.

4) I'm getting failures in the regression suite - apparently the patch
somehow affects costing of index only scans, so that a several queries
switch from index only scans to bitmap index scans etc. I haven't
investigated this more closely, but it seems quite consistent (and I
don't see it without the patch). It seems the patch delays building of
visibility map, or something like that.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Pavel Stehule
pavel.stehule@gmail.com
In reply to: Tomas Vondra (#2)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

2016-07-30 1:46 GMT+02:00 Tomas Vondra <tomas.vondra@2ndquadrant.com>:

Hi,

On 07/29/2016 01:15 PM, Aleksander Alekseev wrote:

Hello

Some time ago we discussed an idea of "fast temporary tables":

/messages/by-id/20160301182500.2c81c3dc@fujitsu

In two words the idea is following.

<The Idea>

PostgreSQL stores information about all relations in pg_catalog. Some
applications create and delete a lot of temporary tables. It causes a
bloating of pg_catalog and running auto vacuum on it. It's quite an
expensive operation which affects entire database performance.

We could introduce a new type of temporary tables. Information about
these tables is stored not in a catalog but in backend's memory. This
way user can solve a pg_catalog bloating problem and improve overall
database performance.

</The Idea>

Great! Thanks for the patch, this is definitely an annoying issue worth
fixing. I've spent a bit of time looking at the patch today, comments below
...

Yes, it some what we need long time

I took me a few months but eventually I made it work. Attached patch
has some flaws. I decided not to invest a lot of time in documenting
it or pgindent'ing all files yet. In my experience it will be rewritten
entirely 3 or 4 times before merging anyway :) But it _works_ and
passes all tests I could think of, including non-trivial cases like
index-only or bitmap scans of catalog tables.

Well, jokes aside, that's a pretty lousy excuse for not writing any docs,
and you're pretty much asking the reviewers to reverse-engineer your
reasoning. So I doubt you'll get many serious reviews without fixing this
gap.

Usage example:

```
CREATE FAST TEMP TABLE fasttab_test1(x int, s text);

INSERT INTO fasttab_test1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc');

UPDATE fasttab_test1 SET s = 'ddd' WHERE x = 2;

DELETE FROM fasttab_test1 WHERE x = 3;

SELECT * FROM fasttab_test1 ORDER BY x;

DROP TABLE fasttab_test1;
```

More sophisticated examples could be find in regression tests:

./src/test/regress/sql/fast_temp.sql

Any feedback on this patch will be much appreciated!

1) I wonder whether the FAST makes sense - does this really change the
performance significantly? IMHO you only move the catalog rows to memory,
so why should the tables be any faster? I also believe this conflicts with
SQL standard specification of CREATE TABLE.

Probably has zero value to have slow and fast temp tables (from catalogue
cost perspective). So the FAST implementation should be used everywhere.
But there are some patterns used with work with temp tables,that should not
working, and we would to decide if we prepare workaround or not.

-- problematic pattern (old code)
IF NOT EXISTS(SELECT * FROM pg_class WHERE ....) THEN
CREATE TEMP TABLE xxx()
ELSE
TRUNCATE TABLE xxx;
END IF;

-- modern patter (new code)
BEGIN
TRUNCATE TABLE xxx;
EXCEPTION WHEN ..... THEN
CREATE TEMP TABLE(...)
END;

In this case we can use GUC, because visible behave should be same.

The benefit of zero catalogue cost temp tables is significant - and for
some larger applications the temp tables did hard performance issues.

2) Why do we need the new relpersistence value? ISTM we could easily got
with just RELPERSISTENCE_TEMP, which would got right away of many chances
as the steps are exactly the same.

IMHO if this patch gets in, we should use it as the only temp table
implementation (Or can you think of cases where keeping rows in pg_class
has advantages?). That'd also eliminate the need for FAST keyword in the
CREATE TABLE command.

The one thin I'm not sure about is that our handling of temporary tables
is not standard compliant - we require each session to create it's own
private temporary table. Moving the rows from pg_class into backend memory
seems to go in the opposite direction, but as no one was planning to fix
this, I don't think it matters much.

3) I think the heapam/indexam/xact and various other places needs a major
rework. You've mostly randomly sprinkled the code with function calls to
make the patch work - that's fine for an initial version, but a more
principled approach is needed.

4) I'm getting failures in the regression suite - apparently the patch
somehow affects costing of index only scans, so that a several queries
switch from index only scans to bitmap index scans etc. I haven't
investigated this more closely, but it seems quite consistent (and I don't
see it without the patch). It seems the patch delays building of visibility
map, or something like that.

Some other random notes:

1. With this code should not be hard to implement global temp tables -
shared persistent structure, temp local data - significant help for any who
have to migrate from Oracle.

2. This should to work on slaves - it is one of ToDo

3. I didn't see support for memory store for column's statistics. Some
separate questions is about production statistics - pg_stat_user_table, ..

Great and important work, thank you

Pavel

Show quoted text

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Pavel Stehule (#3)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 07/30/2016 06:49 AM, Pavel Stehule wrote:

1) I wonder whether the FAST makes sense - does this really change
the performance significantly? IMHO you only move the catalog rows
to memory, so why should the tables be any faster? I also believe
this conflicts with SQL standard specification of CREATE TABLE.

Probably has zero value to have slow and fast temp tables (from
catalogue cost perspective). So the FAST implementation should be used
everywhere. But there are some patterns used with work with temp
tables,that should not working, and we would to decide if we prepare
workaround or not.

-- problematic pattern (old code)
IF NOT EXISTS(SELECT * FROM pg_class WHERE ....) THEN
CREATE TEMP TABLE xxx()
ELSE
TRUNCATE TABLE xxx;
END IF;

I'd argue that if you mess with catalogs directly, you're on your own.
Not only it's fragile, but this pattern is also prone to race conditions
(although a concurrent session can't create a conflicting temporary table).

-- modern patter (new code)
BEGIN
TRUNCATE TABLE xxx;
EXCEPTION WHEN ..... THEN
CREATE TEMP TABLE(...)
END;

In this case we can use GUC, because visible behave should be same.

What GUC?

The benefit of zero catalogue cost temp tables is significant - and for
some larger applications the temp tables did hard performance issues.

Yeah, catalog bloat is a serious issue in such cases, and it's amplified
by indexes created on the temporary tables.

Some other random notes:

1. With this code should not be hard to implement global temp tables -
shared persistent structure, temp local data - significant help for any
who have to migrate from Oracle.

The patch moves in pretty much the opposite direction - if anything,
it'll make it more difficult to implement global temporary tables,
because it removes the definitions from the catalog, thus impossible to
share by catalogs. To get global temporary tables, I think the best
approach would be to share the catalog definition and only override the
filename. Or something like that.

2. This should to work on slaves - it is one of ToDo

No, it does not work on slaves, because it still does a read-write
transaction.

test=# begin read only;
BEGIN
test=# create fast temporary table x (id int);
ERROR: cannot execute CREATE TABLE in a read-only transaction

No idea how difficult it'd be to make it work.

3. I didn't see support for memory store for column's statistics. Some
separate questions is about production statistics - pg_stat_user_table, ..

That seems to work (both analyze and pg_stat_user_tables). Not sure
where it's in the code, and I'm not willing to reverse engineer it.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Tom Lane
tgl@sss.pgh.pa.us
In reply to: Pavel Stehule (#3)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Pavel Stehule <pavel.stehule@gmail.com> writes:

But there are some patterns used with work with temp tables,that should not
working, and we would to decide if we prepare workaround or not.

-- problematic pattern (old code)
IF NOT EXISTS(SELECT * FROM pg_class WHERE ....) THEN
CREATE TEMP TABLE xxx()
ELSE
TRUNCATE TABLE xxx;
END IF;

-- modern patter (new code)
BEGIN
TRUNCATE TABLE xxx;
EXCEPTION WHEN ..... THEN
CREATE TEMP TABLE(...)
END;

If the former stops working, that's a sufficient reason to reject the
patch: it hasn't been thought through carefully enough. The key reason
why I don't think that's negotiable is that if there aren't (apparently)
catalog entries corresponding to the temp tables, that will almost
certainly break many things in the backend and third-party extensions,
not only user code patterns like this one. We'd constantly be fielding
bug reports that "feature X doesn't work with temp tables anymore".

In short, I think that the way to make something like this work is to
figure out how to have "virtual" catalog rows describing a temp table.
Or maybe to partition the catalogs so that vacuuming away temp-table
rows is easier/cheaper than today.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#6David Steele
david@pgmasters.net
In reply to: Tom Lane (#5)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 7/30/16 10:47 AM, Tom Lane wrote:

Pavel Stehule <pavel.stehule@gmail.com> writes:

But there are some patterns used with work with temp tables,that should not
working, and we would to decide if we prepare workaround or not.

-- problematic pattern (old code)
IF NOT EXISTS(SELECT * FROM pg_class WHERE ....) THEN
CREATE TEMP TABLE xxx()
ELSE
TRUNCATE TABLE xxx;
END IF;

-- modern patter (new code)
BEGIN
TRUNCATE TABLE xxx;
EXCEPTION WHEN ..... THEN
CREATE TEMP TABLE(...)
END;

If the former stops working, that's a sufficient reason to reject the
patch: it hasn't been thought through carefully enough. The key reason
why I don't think that's negotiable is that if there aren't (apparently)
catalog entries corresponding to the temp tables, that will almost
certainly break many things in the backend and third-party extensions,
not only user code patterns like this one. We'd constantly be fielding
bug reports that "feature X doesn't work with temp tables anymore".

In short, I think that the way to make something like this work is to
figure out how to have "virtual" catalog rows describing a temp table.
Or maybe to partition the catalogs so that vacuuming away temp-table
rows is easier/cheaper than today.

In addition the latter pattern burns an xid which can be a problem for
high-volume databases.

How about CREATE TEMP TABLE IF NOT EXISTS...?

--
-David
david@pgmasters.net

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Tom Lane (#5)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 07/30/2016 04:47 PM, Tom Lane wrote:

Pavel Stehule <pavel.stehule@gmail.com> writes:

But there are some patterns used with work with temp tables,that should not
working, and we would to decide if we prepare workaround or not.

-- problematic pattern (old code)
IF NOT EXISTS(SELECT * FROM pg_class WHERE ....) THEN
CREATE TEMP TABLE xxx()
ELSE
TRUNCATE TABLE xxx;
END IF;

-- modern patter (new code)
BEGIN
TRUNCATE TABLE xxx;
EXCEPTION WHEN ..... THEN
CREATE TEMP TABLE(...)
END;

If the former stops working, that's a sufficient reason to reject the
patch: it hasn't been thought through carefully enough. The key reason
why I don't think that's negotiable is that if there aren't (apparently)
catalog entries corresponding to the temp tables, that will almost
certainly break many things in the backend and third-party extensions,
not only user code patterns like this one. We'd constantly be fielding
bug reports that "feature X doesn't work with temp tables anymore".

Agreed - breaking internal features for temporary tables is not
acceptable. I was thinking more about external code messing with
catalogs, but on second thought we probably need to keep the records in
pg_class anyway.

In short, I think that the way to make something like this work is
to figure out how to have "virtual" catalog rows describing a temp
table. Or maybe to partition the catalogs so that vacuuming away
temp-table rows is easier/cheaper than today.

Yeah, and I think the patch tries to do that, although in a rather
invasive / unprincipled way. But this will only work for the current
behavior (i.e. mostly what SQL standard means by LOCAL). For GLOBAL
temporary tables I think we need to keep physical catalog row, and only
override the storage filename.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#8Aleksander Alekseev
a.alekseev@postgrespro.ru
In reply to: Tomas Vondra (#7)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Hello.

Thanks everyone for great comments!

Well, jokes aside, that's a pretty lousy excuse for not writing any
docs

I think maybe I put it in a wrong way. There are currently a lot of
comments in a code, more then enough to understand how this feature
works. What I meant is that this is not a final version of a patch and
a few paragraphs are yet to be written. At least it's how I see it. If
you believe that some parts of the code are currently hard to understand
and some comments could be improved, please name it and I will be happy
to fix it.

IMHO if this patch gets in, we should use it as the only temp table
implementation (Or can you think of cases where keeping rows in
pg_class has advantages?). That'd also eliminate the need for FAST
keyword in the CREATE TABLE command.

Probably has zero value to have slow and fast temp tables (from
catalogue cost perspective). So the FAST implementation should be used
everywhere.

If there are no objections I see no reason no to do it in a next
version of a patch.

I'm getting failures in the regression suite

I've run regression suite like 10 times in a row in different
environments with different build flags but didn't manage to reproduce
it. Also our DBAs are testing this feature for weeks now on real-world
applications and they didn't report anything like this. Could you
please describe how to reproduce this issue?

This should to work on slaves - it is one of ToDo

Glad you noticed! In fact I'm currently researching a possibility of
using the same approach for creating writable temporary tables on
replicas.

The key reason why I don't think that's negotiable is that if there
aren't (apparently) catalog entries corresponding to the temp tables,
that will almost certainly break many things in the backend and
third-party extensions, not only user code patterns like this one.

In short, I think that the way to make something like this work is to
figure out how to have "virtual" catalog rows describing a temp table.

I'm afraid once again I put it in a wrong way. What I meant by
"Information about these tables is stored not in a catalog but in
backend's memory" is in fact that _records_ of pg_class, pg_type and
other catalog relations are stored in-memory. Naturally this records
are visible to the user (otherwise \d or \d+ would not work) and you
can do queries like ` select * from pg_class where relname = 'tt1' `.
In other words part of the catalog is indeed "virtual".

I didn't see support for memory store for column's statistics. Some
separate questions is about production statistics -
pg_stat_user_table, ..

That seems to work (both analyze and pg_stat_user_tables). Not sure
where it's in the code, and I'm not willing to reverse engineer it.

Right, `ANALYZE temp_table;` and everything else works. Besides
pg_class, pg_type, pg_attribute and other relations pg_statistic
records for temp tables are stored in-memory as well. IIRC a lot of
pg_stat* relations are in fact views and thus don't require any special
support. If you see that some statistics are broken please don't
hesitate to report it and I will fix it.

Hope I answered all questions so far. I look forward to receive more
comments and questions regarding this patch!

--
Best regards,
Aleksander Alekseev

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Aleksander Alekseev (#8)
1 attachment(s)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/01/2016 11:45 AM, Aleksander Alekseev wrote:

Hello.

Thanks everyone for great comments!

Well, jokes aside, that's a pretty lousy excuse for not writing any
docs

I think maybe I put it in a wrong way. There are currently a lot of
comments in a code, more then enough to understand how this feature
works. What I meant is that this is not a final version of a patch and
a few paragraphs are yet to be written. At least it's how I see it. If
you believe that some parts of the code are currently hard to understand
and some comments could be improved, please name it and I will be happy
to fix it.

I don't think there's "a lot of comments in the code", not even
remotely. At least not in the files I looked into - heapam, indexam,
xact etc. There are a few comments in general, and most of them only
comment obvious facts, like "ignore in-memory tuples" right before a
trivial if statement.

What is needed is an overview of the approach, so that the reviewers can
read that first, instead of assembling the knowledge from pieces
scattered over comments in many pieces. But I see the fasttab.c contains
this:

/* TODO TODO comment the general idea - in-memory tuples and indexes,
hooks principle, FasttabSnapshots, etc */

The other thing that needs to happen is you need to modify comments in
front of some of the modified methods - e.g. the comments may need a
paragraph "But when the table is fast temporary, what happens is ..."

IMHO if this patch gets in, we should use it as the only temp table
implementation (Or can you think of cases where keeping rows in
pg_class has advantages?). That'd also eliminate the need for FAST
keyword in the CREATE TABLE command.

Probably has zero value to have slow and fast temp tables (from
catalogue cost perspective). So the FAST implementation should be used
everywhere.

If there are no objections I see no reason no to do it in a next
version of a patch.

I believe there will be a lot of discussion about this.

I'm getting failures in the regression suite

I've run regression suite like 10 times in a row in different
environments with different build flags but didn't manage to reproduce
it. Also our DBAs are testing this feature for weeks now on real-world
applications and they didn't report anything like this. Could you
please describe how to reproduce this issue?

Nothing special:

$ ./configure --prefix=/home/user/pg-temporary --enable-debug \
--enable-cassert

$ make -s clean && make -s -j4 install

$ export PATH=/home/user/pg-temporary/bin:$PATH

$ pg_ctl -D ~/tmp/data-temporary init

$ pg_ctl -D ~/tmp/data-temporary -l ~/temporary.log start

$ make installcheck

I get the failures every time - regression diff attached. The first
failure in "rolenames" is expected, because of clash with existing user
name. The remaining two failures are not.

I only get the failure for "installcheck" but not "check" for some reason.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

regression.diffstext/plain; charset=UTF-8; name=regression.diffsDownload
*** /home/user/work/postgres/src/test/regress/expected/rolenames.out	2016-07-18 20:48:39.421444507 +0200
--- /home/user/work/postgres/src/test/regress/results/rolenames.out	2016-08-01 15:07:04.597000000 +0200
***************
*** 42,47 ****
--- 42,48 ----
  CREATE ROLE "current_user";
  CREATE ROLE "session_user";
  CREATE ROLE "user";
+ ERROR:  role "user" already exists
  CREATE ROLE current_user; -- error
  ERROR:  CURRENT_USER cannot be used as a role name here
  LINE 1: CREATE ROLE current_user;
***************
*** 956,958 ****
--- 957,960 ----
  DROP OWNED BY regress_testrol0, "Public", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
  DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
  DROP ROLE "Public", "None", "current_user", "session_user", "user";
+ ERROR:  current user cannot be dropped

======================================================================

#10Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tomas Vondra (#9)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Tomas Vondra <tomas.vondra@2ndquadrant.com> writes:

What is needed is an overview of the approach, so that the reviewers can
read that first, instead of assembling the knowledge from pieces
scattered over comments in many pieces. But I see the fasttab.c contains
this:

/* TODO TODO comment the general idea - in-memory tuples and indexes,
hooks principle, FasttabSnapshots, etc */

A fairly common answer when some feature needs an implementation overview
is to create a README file for it, or add a new section in an existing
README file.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11Aleksander Alekseev
a.alekseev@postgrespro.ru
In reply to: Tom Lane (#10)
1 attachment(s)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Thanks everyone for your remarks and comments!

Here is an improved version of a patch.

Main changes:
* Patch passes `make installcheck`
* Code is fully commented, also no more TODO's

I wish I sent this version of a patch last time. Now I realize it was
really hard to read and understand. Hope I managed to correct this
flaw. If you believe that some parts of the code are still poorly
commented or could be improved in any other way please let me know.

And as usual, any other comments, remarks or questions are highly
appreciated!

--
Best regards,
Aleksander Alekseev

Attachments:

fast-temporary-tables-v2.patchtext/x-patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 8f5332a..ac272e1 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1693,7 +1693,7 @@
       <entry></entry>
       <entry>
        <literal>p</> = permanent table, <literal>u</> = unlogged table,
-       <literal>t</> = temporary table
+       <literal>t</> = temporary table, <literal>f</> = fast temporary table
       </entry>
      </row>
 
diff --git a/src/backend/access/common/Makefile b/src/backend/access/common/Makefile
index 1fa6de0..56de4dc 100644
--- a/src/backend/access/common/Makefile
+++ b/src/backend/access/common/Makefile
@@ -12,7 +12,7 @@ subdir = src/backend/access/common
 top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
-OBJS = heaptuple.o indextuple.o printtup.o reloptions.o scankey.o \
+OBJS = fasttab.o heaptuple.o indextuple.o printtup.o reloptions.o scankey.o \
 	tupconvert.o tupdesc.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/access/common/fasttab.c b/src/backend/access/common/fasttab.c
new file mode 100644
index 0000000..610790d
--- /dev/null
+++ b/src/backend/access/common/fasttab.c
@@ -0,0 +1,1884 @@
+/*-------------------------------------------------------------------------
+ *
+ * fasttab.c
+ *	  virtual catalog and fast temporary tables
+ *
+ * This file contents imlementation of special type of temporary tables ---
+ * fast temporary tables (FTT). From user perspective they work exactly as
+ * regular temporary tables. However there are no records about FTTs in
+ * pg_catalog. These records are stored in backend's memory instead and mixed
+ * with regular records during scans of catalog tables. We refer to
+ * corresponding tuples of catalog tables as "in-memory" or "virtual" tuples
+ * and to all these tuples together --- as "in-memory" or "virtual" catalog.
+ *
+ * Note that since temporary tables are visiable only in one session there is
+ * no need to use shared memory or locks for FTTs. Transactions support is
+ * very simple too. There is no need to track xmin/xmax, etc.
+ *
+ * FTTs are designed to to solve pg_catalog bloating problem. The are
+ * applications that create and delete a lot of temporary tables. It causes
+ * bloating of pg_catalog and running auto vacuum on it. It's quite an
+ * expensive operation that affects entire database performance.
+ *
+ * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/access/common/fasttab.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "c.h"
+#include "postgres.h"
+#include "pgstat.h"
+#include "miscadmin.h"
+#include "access/amapi.h"
+#include "access/fasttab.h"
+#include "access/relscan.h"
+#include "access/valid.h"
+#include "access/sysattr.h"
+#include "access/htup_details.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_depend.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_statistic.h"
+#include "storage/bufmgr.h"
+#include "utils/rel.h"
+#include "utils/inval.h"
+#include "utils/memutils.h"
+
+/*****************************************************************************
+		  TYPEDEFS, MACRO DECLARATIONS AND CONST STATIC VARIABLES
+ *****************************************************************************/
+
+/* #define FASTTAB_DEBUG 1 */
+
+#ifdef FASTTAB_DEBUG
+static int32 fasttab_scan_tuples_counter = -1;
+#endif
+
+/* List of in-memory tuples. */
+typedef struct
+{
+	dlist_node	node;
+	HeapTuple	tup;
+}	DListHeapTupleData;
+
+typedef DListHeapTupleData *DListHeapTuple;
+
+/* Like strcmp but for integer types --- int, uint32, Oid, etc. */
+#define FasttabCompareInts(x, y) ( (x) == (y) ? 0 : ( (x) > (y) ? 1 : -1 ))
+
+/* Forward declaration is required for relation_is_inmem_tuple_function */
+struct FasttabSnapshotData;
+typedef struct FasttabSnapshotData *FasttabSnapshot;
+
+/* Predicate that determines whether given tuple should be stored in-memory */
+typedef bool (*relation_is_inmem_tuple_function)
+			(Relation relation, HeapTuple tup, FasttabSnapshot fasttab_snapshot,
+						 int tableIdx);
+
+/* Capacity of FasttabRelationMethods->attrNumbers, see below */
+#define FasttabRelationMaxOidAttributes 2
+
+/* FasttabRelationMethodsTable entry */
+typedef const struct
+{
+	/* relation oid */
+	Oid			relationId;
+	/* predicate that determines whether tuple should be stored in-memory */
+	relation_is_inmem_tuple_function is_inmem_tuple_fn;
+	/* number of attributes in attrNumbers array */
+	AttrNumber	noidattr;
+	/* attributes that reference to pg_class records */
+	AttrNumber	attrNumbers[FasttabRelationMaxOidAttributes];
+}	FasttabRelationMethodsData;
+
+typedef FasttabRelationMethodsData const *FasttabRelationMethods;
+
+/* Forward declaration of all possible is_inmem_tuple_fn values */
+static bool generic_is_inmem_tuple(Relation relation, HeapTuple tup,
+					   FasttabSnapshot fasttab_snapshot, int tableIdx);
+static bool pg_class_is_inmem_tuple(Relation relation, HeapTuple tup,
+						FasttabSnapshot fasttab_snapshot, int tableIdx);
+
+/*
+ * Static information necessary to determine whether given tuple of given
+ * relation should be stored in-memory or not.
+ *
+ * NB: Keep this array sorted by relationId.
+ */
+static FasttabRelationMethodsData FasttabRelationMethodsTable[] =
+{
+	/* 1247 */
+	{TypeRelationId, &generic_is_inmem_tuple, 1,
+		{Anum_pg_type_typrelid, 0}
+	},
+	/* 1249 */
+	{AttributeRelationId, &generic_is_inmem_tuple, 1,
+		{Anum_pg_attribute_attrelid, 0}
+	},
+	/* 1259 */
+	{RelationRelationId, &pg_class_is_inmem_tuple, 0,
+		{0, 0}
+	},
+	/* 2608 */
+	{DependRelationId, &generic_is_inmem_tuple, 2,
+		{Anum_pg_depend_objid, Anum_pg_depend_refobjid}
+	},
+	/* 2611 */
+	{InheritsRelationId, &generic_is_inmem_tuple, 2,
+		{Anum_pg_inherits_inhrelid, Anum_pg_inherits_inhparent}
+	},
+	/* 2619 */
+	{StatisticRelationId, &generic_is_inmem_tuple, 1,
+		{Anum_pg_statistic_starelid, 0}
+	},
+};
+
+/* Number of tables that can have a virtual part */
+#define FasttabSnapshotTablesNumber (lengthof(FasttabRelationMethodsTable))
+
+/* Possible values of FasttabIndexMethods->attrCompareMethod[], see below */
+typedef enum FasttabCompareMethod
+{
+	CompareInvalid,				/* invalid value */
+	CompareOid,					/* compare attributes as oids */
+	CompareCString,				/* compare attributes as strings */
+	CompareInt16,				/* compare attributes as int16's */
+	CompareInt64,				/* compare attributes as int64's */
+	CompareBoolean,				/* compare attributes as booleans */
+}	FasttabCompareMethod;
+
+/* Capacity of FasttabIndexMethods->attrNumbers, see below */
+#define FasttabIndexMaxAttributes 3
+
+/*
+ * FasttabIndexMethodsTable entry.
+ *
+ * NB: typedef is located in fasttab.h
+ */
+struct FasttabIndexMethodsData
+{
+	/* index oid */
+	Oid			indexId;
+	/* number of indexed attributes */
+	AttrNumber	nattr;
+	/* indexed attributes (NB: attribute number can be negative) */
+	AttrNumber	attrNumbers[FasttabIndexMaxAttributes];
+	/* how to compare attributes */
+	FasttabCompareMethod attrCompareMethod[FasttabIndexMaxAttributes];
+};
+
+/*
+ * Static information required for sorting virtual tuples during index scans.
+ *
+ * NB: Keep this array sorted by indexId.
+ *
+ * NB: Uniqueness information is currently not used. Still please keep
+ * comments regarding uniqueness, for possible use in the future.
+ */
+static FasttabIndexMethodsData FasttabIndexMethodsTable[] =
+{
+	/* 2187, non-unique */
+	{InheritsParentIndexId, 1,
+		{Anum_pg_inherits_inhparent, 0, 0},
+		{CompareOid, CompareInvalid, CompareInvalid}
+	},
+	/* 2658, unique */
+	{AttributeRelidNameIndexId, 2,
+		{Anum_pg_attribute_attrelid, Anum_pg_attribute_attname, 0},
+		{CompareOid, CompareCString, CompareInvalid}
+	},
+	/* 2659, unique */
+	{AttributeRelidNumIndexId, 2,
+		{Anum_pg_attribute_attrelid, Anum_pg_attribute_attnum, 0},
+		{CompareOid, CompareInt16, CompareInvalid}
+	},
+	/* 2662, unique */
+	{ClassOidIndexId, 1,
+		{ObjectIdAttributeNumber, 0, 0},
+		{CompareOid, CompareInvalid, CompareInvalid}
+	},
+	/* 2663, unique */
+	{ClassNameNspIndexId, 2,
+		{Anum_pg_class_relname, Anum_pg_class_relnamespace, 0},
+		{CompareCString, CompareOid, CompareInvalid}
+	},
+	/* 2673, non-unique */
+	{DependDependerIndexId, 3,
+		{Anum_pg_depend_classid, Anum_pg_depend_objid, Anum_pg_depend_objsubid},
+		{CompareOid, CompareOid, CompareInt64}
+	},
+	/* 2674, non-unique */
+	{DependReferenceIndexId, 3,
+		{Anum_pg_depend_refclassid, Anum_pg_depend_refobjid,
+		Anum_pg_depend_refobjsubid},
+		{CompareOid, CompareOid, CompareInt64}
+	},
+	/* 2680, unique */
+	{InheritsRelidSeqnoIndexId, 2,
+		{Anum_pg_inherits_inhrelid, Anum_pg_inherits_inhseqno, 0},
+		{CompareOid, CompareOid, CompareInvalid}
+	},
+	/* 2696, unique */
+	{StatisticRelidAttnumInhIndexId, 3,
+		{Anum_pg_statistic_starelid, Anum_pg_statistic_staattnum,
+		Anum_pg_statistic_stainherit},
+		{CompareOid, CompareInt16, CompareBoolean}
+	},
+	/* 2703, unique */
+	{TypeOidIndexId, 1,
+		{ObjectIdAttributeNumber, 0, 0},
+		{CompareOid, CompareInvalid, CompareInvalid}
+	},
+	/* 2704, unique */
+	{TypeNameNspIndexId, 2,
+		{Anum_pg_type_typname, Anum_pg_type_typnamespace, 0},
+		{CompareCString, CompareOid, CompareInvalid}
+	},
+	/* 3455, non-unique */
+	{ClassTblspcRelfilenodeIndexId, 2,
+		{Anum_pg_class_reltablespace, Anum_pg_class_relfilenode, 0},
+		{CompareOid, CompareOid, CompareInvalid}
+	},
+};
+
+/* List of virtual tuples of single relation */
+typedef struct
+{
+	int			tuples_num;		/* number of virtual tuples */
+	dlist_head	tuples;			/* list of virtual tuples */
+}	FasttabSnapshotRelationData;
+
+/*
+ * Snapshot represents state of virtual heap for current transaction or
+ * savepoint.
+ */
+struct FasttabSnapshotData
+{
+	/* Previous snapshot to rollback to. */
+	struct FasttabSnapshotData *prev;
+	/* Optional name of a savepoint. Can be NULL. */
+	char	   *name;
+	/* State of relations that can contain virtual tuples */
+	FasttabSnapshotRelationData relationData[FasttabSnapshotTablesNumber];
+}	FasttabSnapshotData;
+
+/* Determine whether given snapshot is a root snapshot. */
+#define FasttabSnapshotIsRoot(sn) ( !PointerIsValid((sn)->prev) )
+
+/* Determine whether given snapshot is anonymous. */
+#define FasttabSnapshotIsAnonymous(sn) ( !PointerIsValid((sn)->name) )
+
+/* Determine whether there is a transaction in progress. */
+#define FasttabTransactionInProgress() \
+	( PointerIsValid(FasttabSnapshotGetCurrent()->prev))
+
+/*****************************************************************************
+							 GLOBAL VARIABLES
+ *****************************************************************************/
+
+/* Memory context used to store virtual catalog */
+static MemoryContext LocalMemoryContextPrivate = NULL;
+
+/* Counters used to generate unique virtual ItemPointers */
+static uint32 CurrentFasttabBlockId = 0;
+static uint16 CurrentFasttabOffset = 1; /* NB: 0 is considered invalid */
+
+/* Current snapshot */
+static FasttabSnapshot CurrentFasttabSnapshotPrivate = NULL;
+
+/* Current relpersistence hint value */
+static char CurrentRelpersistenceHint = RELPERSISTENCE_UNDEFINED;
+
+/*****************************************************************************
+							UTILITY PROCEDURES
+ *****************************************************************************/
+
+/*
+ * Set relpersistence hint.
+ *
+ * Usualy to figure out wheter tuple should be stored in-memory or not we use
+ * in-memory part of pg_class table. Unfortunately during table creation some
+ * tuples are stored in catalog tables _before_ modification of pg_class table.
+ * So there is no way to tell that these tuples should be in-memory.
+ *
+ * In these rare cases we set a hint with relperistence value of a table we
+ * about to create. This not only solves a problem described above but also
+ * allows to run described check much faster.
+ */
+void
+fasttab_set_relpersistence_hint(char relpersistence)
+{
+	CurrentRelpersistenceHint = relpersistence;
+}
+
+/*
+ * Clear relpersisntence hint.
+ */
+void
+fasttab_clear_relpersistence_hint(void)
+{
+	CurrentRelpersistenceHint = RELPERSISTENCE_UNDEFINED;
+}
+
+/*
+ * Get memory context for storing virtual catalog. Create one if necessary.
+ */
+static MemoryContext
+GetLocalMemoryContext(void)
+{
+	if (!PointerIsValid(LocalMemoryContextPrivate))
+	{
+		LocalMemoryContextPrivate = AllocSetContextCreate(
+														  NULL,
+											"Virtual catalog memory context",
+													ALLOCSET_DEFAULT_MINSIZE,
+												   ALLOCSET_DEFAULT_INITSIZE,
+												   ALLOCSET_DEFAULT_MAXSIZE);
+	}
+
+	return LocalMemoryContextPrivate;
+}
+
+/*
+ * Generate unique virtual ItemPointer
+ */
+static ItemPointerData
+GenFasttabItemPointerData(void)
+{
+	ItemPointerData res;
+
+	BlockIdSet(&(res.ip_blkid), CurrentFasttabBlockId);
+	res.ip_posid = CurrentFasttabOffset | FASTTAB_ITEM_POINTER_BIT;
+
+	CurrentFasttabOffset++;
+
+	if (CurrentFasttabOffset > MaxHeapTuplesPerPage)
+	{
+		CurrentFasttabOffset = 1;
+		CurrentFasttabBlockId++;
+
+#ifdef FASTTAB_DEBUG
+		elog(NOTICE, "FASTTAB: GenFasttabItemPointerData, CurrentFasttabOffset > MaxHeapTuplesPerPage (%d), new values - CurrentFasttabOffset = %d, CurrentFasttabBlockId = %d",
+		  MaxHeapTuplesPerPage, CurrentFasttabOffset, CurrentFasttabBlockId);
+#endif
+	}
+
+	return res;
+}
+
+/*
+ * Find FasttabRelationMethodsTable index by relation oid using binary search.
+ *
+ * Not for direct usage. GetSnapshotRelationIdxByOid should be used instead.
+ *
+ * Return values:
+ * == -1 - not found
+ * >=  0 - found on N-th position
+ */
+static int
+GetSnapshotRelationIdxByOidInternal(Oid relId)
+{
+	int			begin = 0;
+	int			end = FasttabSnapshotTablesNumber - 1;
+
+#ifdef USE_ASSERT_CHECKING
+	/* Test that FasttabRelationMethodsTable is properly sorted */
+	int			i;
+
+	for (i = 0; i <= end; i++)
+	{
+		Assert(PointerIsValid(FasttabRelationMethodsTable[i].is_inmem_tuple_fn));
+		if (i > 0)
+			Assert(FasttabRelationMethodsTable[i - 1].relationId < FasttabRelationMethodsTable[i].relationId);
+	}
+#endif
+
+	while (begin < end)
+	{
+		int			test = (begin + end) / 2;
+
+		if (FasttabRelationMethodsTable[test].relationId == relId)
+		{
+			begin = test;
+			break;
+		}
+
+		if (FasttabRelationMethodsTable[test].relationId < relId)
+			begin = test + 1;	/* go right */
+		else
+			end = test - 1;		/* go left */
+	}
+
+	if (FasttabRelationMethodsTable[begin].relationId == relId)
+		return begin;			/* found */
+	else
+		return -1;				/* not found */
+}
+
+/*
+ * Determine FasttabRelationMethodsTable index by relation oid.
+ */
+static inline int
+GetSnapshotRelationIdxByOid(Oid relId)
+{
+	int			result;
+
+	Assert(IsFasttabHandledRelationId(relId));
+	result = GetSnapshotRelationIdxByOidInternal(relId);
+	Assert(result >= 0 && result < FasttabSnapshotTablesNumber);
+	return result;
+}
+
+/*
+ * Determine whether relation with given oid can have virtual tuples.
+ */
+bool
+IsFasttabHandledRelationId(Oid relId)
+{
+	return (GetSnapshotRelationIdxByOidInternal(relId) >= 0);
+}
+
+/*
+ * Find FasttabIndexMethodsTable entry by index oid using binary search.
+ *
+ * Not for direct usage. GetFasttabIndexMethods should be used instead.
+ *
+ * Return values:
+ * == NULL - not found
+ * != NULL - found
+ */
+static FasttabIndexMethods
+GetFasttabIndexMethodsInternal(Oid indexId)
+{
+	int			begin = 0;
+	int			end = (sizeof(FasttabIndexMethodsTable) /
+					   sizeof(FasttabIndexMethodsTable[0]) - 1);
+
+#ifdef USE_ASSERT_CHECKING
+	/* Test that FasttabIndexMethodsTable is properly sorted. */
+	int			i;
+
+	for (i = 0; i <= end; i++)
+	{
+		if (i > 0)
+			Assert(FasttabIndexMethodsTable[i - 1].indexId < FasttabIndexMethodsTable[i].indexId);
+	}
+#endif
+
+	while (begin < end)
+	{
+		int			test = (begin + end) / 2;
+
+		if (FasttabIndexMethodsTable[test].indexId == indexId)
+		{
+			begin = test;
+			break;
+		}
+
+		if (FasttabIndexMethodsTable[test].indexId < indexId)
+			begin = test + 1;	/* go right */
+		else
+			end = test - 1;		/* go left */
+	}
+
+	if (FasttabIndexMethodsTable[begin].indexId == indexId)
+		return &FasttabIndexMethodsTable[begin];		/* found */
+	else
+		return NULL;			/* not found */
+}
+
+/*
+ * Determine whether index with given oid has a virtual part.
+ */
+bool
+IsFasttabHandledIndexId(Oid indexId)
+{
+	return (GetFasttabIndexMethodsInternal(indexId) != NULL);
+}
+
+/*
+ * Find FasttabIndexMethodsTable entry by index oid using binary search.
+ */
+static inline FasttabIndexMethods
+GetFasttabIndexMethods(Oid indexId)
+{
+	Assert(IsFasttabHandledIndexId(indexId));
+	return GetFasttabIndexMethodsInternal(indexId);
+}
+
+/*
+ * Free single DListHeapTuple
+ */
+static void
+DListHeapTupleFree(DListHeapTuple dlist_tup)
+{
+	heap_freetuple(dlist_tup->tup);
+	pfree(dlist_tup);
+}
+
+/*
+ * Free list of DListHeapTuple's
+ */
+static void
+FasttabDListFree(dlist_head *head)
+{
+	while (!dlist_is_empty(head))
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) dlist_pop_head_node(head);
+
+		DListHeapTupleFree(dlist_tup);
+	}
+}
+
+/*
+ * Create a new empty snapshot.
+ */
+static FasttabSnapshot
+FasttabSnapshotCreateEmpty(void)
+{
+	FasttabSnapshot result;
+	MemoryContext oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+
+	result = palloc0(sizeof(FasttabSnapshotData));
+	MemoryContextSwitchTo(oldctx);
+	return result;
+}
+
+/*
+ * Create a snapshot copy.
+ */
+static FasttabSnapshot
+FasttabSnapshotCopy(FasttabSnapshot src, const char *dst_name)
+{
+	int			idx;
+	dlist_iter	iter;
+	MemoryContext oldctx;
+	FasttabSnapshot dst = FasttabSnapshotCreateEmpty();
+
+	oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+	dst->name = dst_name ? pstrdup(dst_name) : NULL;
+
+	for (idx = 0; idx < FasttabSnapshotTablesNumber; idx++)
+	{
+		dst->relationData[idx].tuples_num = src->relationData[idx].tuples_num;
+		dlist_foreach(iter, &src->relationData[idx].tuples)
+		{
+			DListHeapTuple src_dlist_tup = (DListHeapTuple) iter.cur;
+			DListHeapTuple dst_dlist_tup = palloc(sizeof(DListHeapTupleData));
+
+			dst_dlist_tup->tup = heap_copytuple(src_dlist_tup->tup);
+			dlist_push_tail(&dst->relationData[idx].tuples,
+							&dst_dlist_tup->node);
+		}
+	}
+
+	MemoryContextSwitchTo(oldctx);
+	return dst;
+}
+
+/*
+ * Free snapshot.
+ */
+static void
+FasttabSnapshotFree(FasttabSnapshot fasttab_snapshot)
+{
+	int			idx;
+
+	for (idx = 0; idx < FasttabSnapshotTablesNumber; idx++)
+		FasttabDListFree(&fasttab_snapshot->relationData[idx].tuples);
+
+	if (PointerIsValid(fasttab_snapshot->name))
+		pfree(fasttab_snapshot->name);
+
+	pfree(fasttab_snapshot);
+}
+
+/*
+ * Get current snapshot. Create one if necessary.
+ */
+static FasttabSnapshot
+FasttabSnapshotGetCurrent(void)
+{
+	if (!PointerIsValid(CurrentFasttabSnapshotPrivate))
+		CurrentFasttabSnapshotPrivate = FasttabSnapshotCreateEmpty();
+
+	return CurrentFasttabSnapshotPrivate;
+}
+
+/*
+ * Places a snapshot on top of snapshots stack. Placed snapshot becomes
+ * current.
+ */
+static inline void
+FasttabSnapshotPushBack(FasttabSnapshot fasttab_snapshot)
+{
+	fasttab_snapshot->prev = FasttabSnapshotGetCurrent();
+	CurrentFasttabSnapshotPrivate = fasttab_snapshot;
+}
+
+/*
+ * Removes snapshot from top of snapshots stack.
+ *
+ * Returns valid FasttabSnapshot or NULL if only root snapshot left.
+ */
+static FasttabSnapshot
+FasttabSnapshotPopBack(void)
+{
+	FasttabSnapshot curr = FasttabSnapshotGetCurrent();
+
+	if (FasttabSnapshotIsRoot(curr))
+		return NULL;
+
+	CurrentFasttabSnapshotPrivate = curr->prev;
+	curr->prev = NULL;
+	return curr;
+}
+
+/*
+ * Creates a copy of current snapshot with given name (can be NULL) and places
+ * it on top of snapshots stack. This copy becomes current snapshot.
+ */
+static void
+FasttabSnapshotCreate(const char *name)
+{
+	FasttabSnapshot src = FasttabSnapshotGetCurrent();
+	FasttabSnapshot dst = FasttabSnapshotCopy(src, name);
+
+	FasttabSnapshotPushBack(dst);
+}
+
+/*
+ * Makes given snapshot a root one.
+ */
+static void
+FasttabSnapshotPushFront(FasttabSnapshot fasttab_snapshot)
+{
+	FasttabSnapshot temp = FasttabSnapshotGetCurrent();
+
+	while (!FasttabSnapshotIsRoot(temp))
+		temp = temp->prev;
+
+	temp->prev = fasttab_snapshot;
+	fasttab_snapshot->prev = NULL;
+}
+
+/*****************************************************************************
+							 MAIN PROCEDURES
+ *****************************************************************************/
+
+/*
+ * Make preparations related to virtual catalog on transaction begin.
+ *
+ * NB: There could be already a transaction in progress.
+ */
+void
+fasttab_begin_transaction(void)
+{
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_begin_transaction, transaction is already in progress: %u",
+		 FasttabTransactionInProgress());
+#endif
+
+	if (FasttabTransactionInProgress())
+		return;
+
+	/* begin transaction */
+	FasttabSnapshotCreate(NULL);
+	Assert(FasttabTransactionInProgress());
+	Assert(FasttabSnapshotIsAnonymous(FasttabSnapshotGetCurrent()));
+}
+
+/*
+ * Perform actions related to virtual catalog on transaction commit.
+ *
+ * NB: There could be actually no transaction in progress.
+ */
+void
+fasttab_end_transaction(void)
+{
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_end_transaction result = %u (1 - commit, 0 - rollback)"
+		 ", transaction is in progress: %u", result, FasttabTransactionInProgress());
+#endif
+
+	if (!FasttabTransactionInProgress())
+		return;
+
+	Assert(FasttabSnapshotIsAnonymous(FasttabSnapshotGetCurrent()));
+
+	/* Commit transaction. 1) Save top snapshot to the bottom of the stack. */
+	FasttabSnapshotPushFront(FasttabSnapshotPopBack());
+	/* 2) get rid of all snapshots except the root one */
+	fasttab_abort_transaction();
+}
+
+/*
+ * Perform actions related to virtual catalog on transaction abort.
+ *
+ * NB: There could be in fact no transaction running.
+ */
+void
+fasttab_abort_transaction(void)
+{
+	FasttabSnapshot fasttab_snapshot;
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_abort_transaction, transaction is in progress: %u (it's OK if this procedure is called from fasttab_end_transaction - see the code)",
+		 FasttabTransactionInProgress());
+#endif
+
+	if (!FasttabTransactionInProgress())
+		return;
+
+	for (;;)
+	{
+		fasttab_snapshot = FasttabSnapshotPopBack();
+		if (!fasttab_snapshot)	/* root snapshot reached */
+			break;
+
+		FasttabSnapshotFree(fasttab_snapshot);
+	}
+
+	Assert(!FasttabTransactionInProgress());
+}
+
+/*
+ * Perform actions related to virtual catalog on savepoint creation.
+ */
+void
+fasttab_define_savepoint(const char *name)
+{
+	Assert(FasttabTransactionInProgress());
+	Assert(FasttabSnapshotIsAnonymous(FasttabSnapshotGetCurrent()));
+
+	/*
+	 * Value of `name` argument can be NULL in 'rollback to savepoint' case.
+	 * This case is already handled by fasttab_rollback_to_savepoint.
+	 */
+	if (!PointerIsValid(name))
+		return;
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_define_safepoint, name = '%s'", name);
+#endif
+
+	FasttabSnapshotCreate(name);	/* savepoint to rollback to */
+	FasttabSnapshotCreate(NULL);	/* current snapshot to store changes */
+
+	Assert(FasttabTransactionInProgress());
+}
+
+/*
+ * Perform actions related to virtual catalog on `rollback to savepoint`.
+ *
+ * NB: There is no need to re-check case of savepoint name (upper / lower) or
+ * that savepoint exists.
+ */
+void
+fasttab_rollback_to_savepoint(const char *name)
+{
+	Assert(PointerIsValid(name));
+	Assert(FasttabTransactionInProgress());
+	Assert(FasttabSnapshotIsAnonymous(FasttabSnapshotGetCurrent()));
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_rollback_to_savepoint, name = '%s'", name);
+#endif
+
+	/*
+	 * Pop snapshots from the stack and free them until a snapshot with given
+	 * name will be reached.
+	 */
+	for (;;)
+	{
+		FasttabSnapshot fasttab_snapshot = FasttabSnapshotGetCurrent();
+
+		Assert(!FasttabSnapshotIsRoot(fasttab_snapshot));
+
+		if ((!FasttabSnapshotIsAnonymous(fasttab_snapshot)) &&
+			(strcmp(fasttab_snapshot->name, name) == 0))
+			break;
+
+		FasttabSnapshotFree(FasttabSnapshotPopBack());
+	}
+
+	/* Create a new current snapshot to store changes. */
+	FasttabSnapshotCreate(NULL);
+}
+
+/*
+ * (Re)initialize part of `scan` related to virtual catalog during heap
+ * (re)scan.
+ */
+void
+fasttab_beginscan(HeapScanDesc scan)
+{
+	int			idx;
+	Oid			relid = RelationGetRelid(scan->rs_rd);
+	FasttabSnapshot fasttab_snapshot;
+
+	if (!IsFasttabHandledRelationId(relid))
+		return;
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+
+	idx = GetSnapshotRelationIdxByOid(relid);
+	if (dlist_is_empty(&fasttab_snapshot->relationData[idx].tuples))
+		scan->rs_curr_inmem_tupnode = NULL;
+	else
+		scan->rs_curr_inmem_tupnode = dlist_head_node(&fasttab_snapshot->relationData[idx].tuples);
+
+#ifdef FASTTAB_DEBUG
+	fasttab_scan_tuples_counter = 0;
+	elog(NOTICE, "FASTTAB: fasttab_beginscan, returning scan = %p, rs_curr_inmem_tupnode = %p", scan, scan->rs_curr_inmem_tupnode);
+#endif
+}
+
+/*
+ * Returns next virtual tuple during heap scan or NULL if there are no more
+ * virtual tuples. Basically heap_getnext implementation for virtual catalog.
+ */
+HeapTuple
+fasttab_getnext(HeapScanDesc scan, ScanDirection direction)
+{
+	bool		match;
+	int			idx;
+	FasttabSnapshot fasttab_snapshot;
+	DListHeapTuple dlist_tup;
+	dlist_node *ret_node;
+
+	if (!IsFasttabHandledRelationId(RelationGetRelid(scan->rs_rd)))
+		return NULL;
+
+	/* Other directions are never used for pg_catalog. */
+	Assert(ScanDirectionIsForward(direction));
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(scan->rs_rd));
+
+	/*
+	 * Simple strategy - first return all in-memory tuples, then proceed with
+	 * others.
+	 */
+	while (scan->rs_curr_inmem_tupnode) /* inmemory tuples enumiration is
+										 * still in progress? */
+	{
+		ret_node = scan->rs_curr_inmem_tupnode;
+
+		if (dlist_has_next(&fasttab_snapshot->relationData[idx].tuples, ret_node))
+			scan->rs_curr_inmem_tupnode = dlist_next_node(&fasttab_snapshot->relationData[idx].tuples, ret_node);
+		else
+			scan->rs_curr_inmem_tupnode = NULL;
+
+		dlist_tup = (DListHeapTuple) ret_node;
+
+#ifdef FASTTAB_DEBUG
+		fasttab_scan_tuples_counter++;
+		elog(NOTICE, "FASTTAB: fasttab_getnext, scan = %p, counter = %u, direction = %d, return tuple t_self = %08X/%04X, oid = %d",
+			 scan, fasttab_scan_tuples_counter, direction,
+			 BlockIdGetBlockNumber(&dlist_tup->tup->t_self.ip_blkid), dlist_tup->tup->t_self.ip_posid, HeapTupleGetOid(dlist_tup->tup)
+			);
+#endif
+
+		/* HeapKeyTest is a macro, it changes `match` variable */
+		HeapKeyTest(dlist_tup->tup, RelationGetDescr(scan->rs_rd), scan->rs_nkeys, scan->rs_key, match);
+		if (!match)
+			continue;
+
+		return dlist_tup->tup;
+	}
+
+	/* There are not more virtual tuples. */
+	return NULL;
+}
+
+/*
+ * Pretend searching HOT chain for virtual tuple.
+ *
+ * Basically heap_hot_search_buffer implementation for virtual catalog.
+ */
+bool
+fasttab_hot_search_buffer(ItemPointer tid, Relation relation,
+						  HeapTuple heapTuple, bool *all_dead, bool *result)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+	bool		found = false;
+
+	if (!IsFasttabItemPointer(tid))
+		return false;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(relation));
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+
+		if (ItemPointerEquals(&dlist_tup->tup->t_self, tid))
+		{
+			memcpy(heapTuple, dlist_tup->tup, sizeof(HeapTupleData));
+			found = true;
+			break;
+		}
+	}
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_hot_search_buffer, tid = %08X/%04X, found = %u",
+		 BlockIdGetBlockNumber(&tid->ip_blkid), tid->ip_posid, found);
+#endif
+
+	/* `all_dead` can be NULL during bitmap scan */
+	if (all_dead)
+		*all_dead = false;
+
+	/* `result` can be false in ALTER TABLE case */
+	*result = found;
+	return true;
+}
+
+/*
+ * Insert a tuple. Basically heap_insert implementation for virtual tuples.
+ * Returns true if tuple was inserted, false otherwise.
+ */
+bool
+fasttab_insert(Relation relation, HeapTuple tup, HeapTuple heaptup, Oid *result)
+{
+	FasttabSnapshot fasttab_snapshot;
+	MemoryContext oldctx;
+	DListHeapTuple dlist_tup;
+	int			idx = GetSnapshotRelationIdxByOidInternal(RelationGetRelid(relation));
+
+	if (idx < 0)				/* i.e. `!IsFasttabHandledRelationId` */
+		return false;
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+
+	/*
+	 * Check whether tuple should be stored in-memory.
+	 *
+	 * NB: passing `idx` is kind of optimization, it could be actually
+	 * re-calculated from `relation` argument.
+	 */
+	if (!FasttabRelationMethodsTable[idx].is_inmem_tuple_fn(relation,
+												 tup, fasttab_snapshot, idx))
+		return false;
+
+	oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+	heaptup->t_self = GenFasttabItemPointerData();
+	dlist_tup = palloc(sizeof(DListHeapTupleData));
+	dlist_tup->tup = heap_copytuple(heaptup);
+	MemoryContextSwitchTo(oldctx);
+
+	dlist_push_tail(&fasttab_snapshot->relationData[idx].tuples,
+					&dlist_tup->node);
+	fasttab_snapshot->relationData[idx].tuples_num++;
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_insert, dlist_tup->tup->t_self = %08X/%04X, oid = %d, inmemory tuples num = %d, heaptup oid = %d, idx = %d, relation relid = %d",
+		 BlockIdGetBlockNumber(&dlist_tup->tup->t_self.ip_blkid),
+		 dlist_tup->tup->t_self.ip_posid, HeapTupleGetOid(dlist_tup->tup),
+		 fasttab_snapshot->relationData[idx].tuples_num,
+		 HeapTupleGetOid(heaptup), idx, RelationGetRelid(relation)
+		);
+#endif
+
+	CacheInvalidateHeapTuple(relation, dlist_tup->tup, NULL);
+	pgstat_count_heap_insert(relation, 1);
+	if (heaptup != tup)
+	{
+		tup->t_self = heaptup->t_self;
+		heap_freetuple(heaptup);
+	}
+
+	*result = HeapTupleGetOid(tup);
+	return true;
+}
+
+/*
+ * Remove pg_depend and pg_type records that would be kept in memory otherwise
+ * when relation with given Oid is deleted. Basically here we are solving the
+ * same issue that is solved by relpersistence hint, but during table deletion,
+ * not creation.
+ *
+ * Used in fasttab_delete.
+ */
+static void
+fasttab_clean_catalog_on_relation_delete(Oid reloid)
+{
+	Oid			curroid = reloid;
+	FasttabSnapshot fasttab_snapshot = FasttabSnapshotGetCurrent();
+	int			dependIdx = GetSnapshotRelationIdxByOid(DependRelationId);
+	int			typeIdx = GetSnapshotRelationIdxByOid(TypeRelationId);
+	Relation	dependRel = relation_open(DependRelationId, AccessShareLock);
+	Relation	typeRel = relation_open(TypeRelationId, AccessShareLock);
+	ItemPointerData itemPointerData;
+
+	for (;;)
+	{
+		dlist_iter	iter;
+		bool		isnull,
+					found = false;
+
+		/* Find pg_depend tuple with refobjid == curroid. */
+		dlist_foreach(iter, &fasttab_snapshot->relationData[dependIdx].tuples)
+		{
+			DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+			Oid			refobjid = DatumGetObjectId(heap_getattr(dlist_tup->tup, Anum_pg_depend_refobjid,
+									  RelationGetDescr(dependRel), &isnull));
+
+			if (refobjid == curroid)
+			{
+				found = true;
+				/* curroid := tuple.objid */
+				curroid = DatumGetObjectId(heap_getattr(dlist_tup->tup, Anum_pg_depend_objid,
+									  RelationGetDescr(dependRel), &isnull));
+
+				/*
+				 * Delete found tuple. Can't pass dlist_tup->tup->t_self as an
+				 * argument - this memory is about to be freed.
+				 */
+				itemPointerData = dlist_tup->tup->t_self;
+				fasttab_delete(dependRel, &itemPointerData);
+				break;
+			}
+		}
+
+		/* If not found - cleanup is done, end of loop */
+		if (!found)
+			break;
+
+		/* Find pg_type tuple with oid == curroid */
+		found = false;
+		dlist_foreach(iter, &fasttab_snapshot->relationData[typeIdx].tuples)
+		{
+			DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+			Oid			oid = DatumGetObjectId(heap_getattr(dlist_tup->tup, ObjectIdAttributeNumber,
+										RelationGetDescr(typeRel), &isnull));
+
+			if (oid == curroid)
+			{
+				found = true;
+
+				/*
+				 * Delete found tuple. Can't pass dlist_tup->tup->t_self as an
+				 * argument - this memory is about to be freed.
+				 */
+				itemPointerData = dlist_tup->tup->t_self;
+				fasttab_delete(typeRel, &itemPointerData);
+				break;
+			}
+		}
+
+		Assert(found);
+	}
+
+	relation_close(typeRel, AccessShareLock);
+	relation_close(dependRel, AccessShareLock);
+}
+
+/*
+ * Delete tuple. Basically heap_delete implementation for virtual tuples.
+ * Returns true if tuple was deleted, false otherwise.
+ */
+bool
+fasttab_delete(Relation relation, ItemPointer tid)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+
+	if (!IsFasttabItemPointer(tid))
+		return false;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(relation));
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+
+		if (ItemPointerEquals(&dlist_tup->tup->t_self, tid))
+		{
+			/*
+			 * If it's a tuple from pg_class, delete tuples that might still
+			 * reference to it.
+			 */
+			if (RelationGetRelid(relation) == RelationRelationId)
+			{
+				bool		isnull;
+				Oid			reloid = DatumGetObjectId(heap_getattr(dlist_tup->tup, ObjectIdAttributeNumber,
+									   RelationGetDescr(relation), &isnull));
+
+				fasttab_clean_catalog_on_relation_delete(reloid);
+			}
+
+			pgstat_count_heap_delete(relation);
+			CacheInvalidateHeapTuple(relation, dlist_tup->tup, NULL);
+
+			dlist_delete(&dlist_tup->node);
+			DListHeapTupleFree(dlist_tup);
+			fasttab_snapshot->relationData[idx].tuples_num--;
+
+#ifdef FASTTAB_DEBUG
+			elog(NOTICE, "FASTTAB: fasttab_delete, tid = %08X/%04X - entry found and deleted, tuples_num = %d, idx = %d, rd_id = %d",
+				 BlockIdGetBlockNumber(&tid->ip_blkid), tid->ip_posid,
+				 fasttab_snapshot->relationData[idx].tuples_num, idx, relation->rd_id
+				);
+#endif
+
+			return true;
+		}
+	}
+
+	elog(ERROR, "in-memory tuple not found during delete");
+	return false;				/* will be never reached */
+}
+
+/*
+ * Update tuple. Basically heap_update implementation for virtual tuples.
+ * Returns true if tuple was updated, false otherwise.
+ */
+bool
+fasttab_update(Relation relation, ItemPointer otid, HeapTuple newtup)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+
+	if (!IsFasttabItemPointer(otid))
+		return false;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_update, looking for otid = %08X/%04X",
+		 BlockIdGetBlockNumber(&otid->ip_blkid), otid->ip_posid);
+#endif
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(relation));
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+
+		if (ItemPointerEquals(&dlist_tup->tup->t_self, otid))
+		{
+			MemoryContext oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+
+			CacheInvalidateHeapTuple(relation, dlist_tup->tup, newtup);
+			heap_freetuple(dlist_tup->tup);
+			newtup->t_self = GenFasttabItemPointerData();
+			dlist_tup->tup = heap_copytuple(newtup);
+			MemoryContextSwitchTo(oldctx);
+
+			pgstat_count_heap_update(relation, false);
+
+#ifdef FASTTAB_DEBUG
+			elog(NOTICE, "FASTTAB: fasttab_update - entry found and updated, newtup->t_self = %08X/%04X, oid = %d, tuples_num = %d, idx = %d",
+				 BlockIdGetBlockNumber(&newtup->t_self.ip_blkid), newtup->t_self.ip_posid,
+				 HeapTupleGetOid(dlist_tup->tup),
+				 fasttab_snapshot->relationData[idx].tuples_num, idx);
+#endif
+			return true;
+		}
+	}
+
+	elog(ERROR, "in-memory tuple not found during update");
+	return false;				/* will be never reached */
+}
+
+/*
+ * Update tuple "in place". Basically heap_inplace_update implementation for
+ * virtual tuples. Returns true if tuple was updated, false otherwise.
+ */
+bool
+fasttab_inplace_update(Relation relation, HeapTuple tuple)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+
+	if (!IsFasttabItemPointer(&tuple->t_self))
+		return false;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_heap_inplace_update, looking for tuple with tid = %08X/%04X, oid = %d...",
+	  BlockIdGetBlockNumber(&tuple->t_self.ip_blkid), tuple->t_self.ip_posid,
+		 HeapTupleGetOid(tuple));
+#endif
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(relation));
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+
+		if (ItemPointerEquals(&dlist_tup->tup->t_self, &tuple->t_self))
+		{
+			MemoryContext oldctx = MemoryContextSwitchTo(GetLocalMemoryContext());
+
+			if (!IsBootstrapProcessingMode())
+				CacheInvalidateHeapTuple(relation, tuple, NULL);
+
+			heap_freetuple(dlist_tup->tup);
+			dlist_tup->tup = heap_copytuple(tuple);
+			MemoryContextSwitchTo(oldctx);
+
+#ifdef FASTTAB_DEBUG
+			elog(NOTICE, "FASTTAB: fasttab_inplace_update - entry found and updated, tuples_num = %d, idx = %d",
+				 fasttab_snapshot->relationData[idx].tuples_num, idx);
+#endif
+			return true;
+		}
+	}
+
+	elog(ERROR, "in-memory tuple not found during \"in place\" update");
+	return false;				/* will be never reached */
+}
+
+/*
+ * Insert an index tuple into a relation. Basically index_insert implementation
+ * for virtual tuples. Returns true if tuple was inserted, false otherwise.
+ *
+ * Current FFTs implementation builds indexes "on the fly" when index scan
+ * begins. Thus for now we do almost nothing here.
+ */
+bool
+fasttab_index_insert(Relation indexRelation, ItemPointer heap_t_ctid,
+					 bool *result)
+{
+	Oid			indexId = RelationGetRelid(indexRelation);
+
+	if (!IsFasttabItemPointer(heap_t_ctid))
+		return false;
+
+	Assert(IsFasttabHandledIndexId(indexId));
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_insert, indexRelation relid = %u, heap_t_ctid = %08X/%04X",
+		 RelationGetRelid(indexRelation),
+		 BlockIdGetBlockNumber(&heap_t_ctid->ip_blkid),
+		 heap_t_ctid->ip_posid);
+#endif
+
+	if (IsFasttabHandledIndexId(indexId))
+	{
+		*result = true;
+		return true;			/* don't actually modify an index */
+	}
+
+	return false;
+}
+
+/*
+ * Compare two tuples during index scan.
+ *
+ * Returns:
+ * > 0 - first tuple is greater
+ * = 0 - tuples are equal
+ * < 0 - first tuple is lesser
+ */
+static int
+fasttab_index_compare_tuples(HeapTuple first, HeapTuple second,
+							 IndexScanDesc scan)
+{
+	TupleDesc	tupledesc = RelationGetDescr(scan->heapRelation);
+	Datum		datum1,
+				datum2;
+	bool		isnull1,
+				isnull2;
+	int			i,
+				result = 0;
+
+	for (i = 0; i < scan->indexMethods->nattr; i++)
+	{
+		Assert(scan->indexMethods->attrCompareMethod[i] != CompareInvalid);
+		datum1 = heap_getattr(first, scan->indexMethods->attrNumbers[i], tupledesc,
+							  &isnull1);
+		datum2 = heap_getattr(second, scan->indexMethods->attrNumbers[i], tupledesc,
+							  &isnull2);
+		Assert((!isnull1) && (!isnull2));
+
+		switch (scan->indexMethods->attrCompareMethod[i])
+		{
+			case CompareOid:
+				result = FasttabCompareInts(DatumGetObjectId(datum1),
+											DatumGetObjectId(datum2));
+				break;
+			case CompareCString:
+				result = strcmp(DatumGetCString(datum1),
+								DatumGetCString(datum2));
+				break;
+			case CompareInt16:
+				result = FasttabCompareInts(DatumGetInt16(datum1),
+											DatumGetInt16(datum2));
+				break;
+			case CompareInt64:
+				result = FasttabCompareInts(DatumGetInt64(datum1),
+											DatumGetInt64(datum2));
+				break;
+			case CompareBoolean:
+				result = FasttabCompareInts(DatumGetBool(datum1),
+											DatumGetBool(datum2));
+				break;
+			default:			/* should never happen, can be useful during
+								 * development though */
+				elog(ERROR, "Unexpected compare method: %d",
+					 scan->indexMethods->attrCompareMethod[i]);
+		}
+
+		if (result != 0)
+			break;
+	}
+
+	return result;
+}
+
+/*
+ * Form index tuple from virtual heap tuple during index-only scan.
+ */
+static IndexTuple
+fasttab_index_form_tuple(HeapTuple tup, IndexScanDesc scan)
+{
+	TupleDesc	heaptupledesc = RelationGetDescr(scan->heapRelation);
+	TupleDesc	indextupledesc = RelationGetDescr(scan->indexRelation);
+	Datum		values[FasttabIndexMaxAttributes];
+	bool		isnull[FasttabIndexMaxAttributes];
+	int			i;
+
+	for (i = 0; i < scan->indexMethods->nattr; i++)
+	{
+		/*
+		 * NB: heap_getattr prcesses negative attribute numbers like
+		 * ObjectIdAttributeNumber just fine
+		 */
+		values[i] = heap_getattr(tup, scan->indexMethods->attrNumbers[i],
+								 heaptupledesc, &(isnull[i]));
+	}
+
+	return index_form_tuple(indextupledesc, values, isnull);
+}
+
+/*
+ * Convert index attribute number to heap attribute number.
+ */
+static inline AttrNumber
+fasttab_convert_index_attno_to_heap_attno(IndexScanDesc scan,
+										  AttrNumber indexAttno)
+{
+	Assert(indexAttno > 0);
+	Assert(indexAttno <= FasttabIndexMaxAttributes);
+	Assert(indexAttno <= scan->indexMethods->nattr);
+	return scan->indexMethods->attrNumbers[indexAttno - 1];
+}
+
+/*
+ * Determine whether virtual heap tuple matches WHERE condition during index
+ * scan.
+ */
+static bool
+fasttab_index_tuple_matches_where_condition(IndexScanDesc scan, HeapTuple tup)
+{
+	int			i;
+	bool		insert;
+	AttrNumber	attrNumbersBackup[FasttabIndexMaxAttributes];
+
+	/* If WHERE condition is empty all tuples match */
+	if (scan->numberOfKeys == 0)
+		return true;
+
+	/* NB: scan->keyData[0].sk_strategy can be InvalidStrategy */
+	Assert(scan->keyData != NULL);
+	Assert(scan->keyData[0].sk_attno != InvalidAttrNumber);
+
+	/* Convert index attribute numbers to tuple attribute numbers. */
+	for (i = 0; i < scan->numberOfKeys; i++)
+	{
+		attrNumbersBackup[i] = scan->keyData[i].sk_attno;
+		scan->keyData[i].sk_attno = fasttab_convert_index_attno_to_heap_attno(scan, scan->keyData[i].sk_attno);
+	}
+
+	/* NB: HeapKeyTest is a macro, it changes `insert` variable */
+	HeapKeyTest(tup, RelationGetDescr(scan->heapRelation), scan->numberOfKeys,
+				scan->keyData, insert);
+
+	/* Restore original attribute numbers. */
+	for (i = 0; i < scan->numberOfKeys; i++)
+		scan->keyData[i].sk_attno = attrNumbersBackup[i];
+
+	return insert;
+}
+
+/*
+ * Add tuple to scan->xs_inmem_tuplist at proper position.
+ *
+ * Returs:
+ * true - tuple added
+ * false - tuple not added (filtered by WHERE condition)
+ */
+static bool
+fasttab_index_insert_tuple_in_sorted_list(IndexScanDesc scan, HeapTuple tup)
+{
+	DListHeapTuple dlist_tup;
+	dlist_node *insert_after = &scan->xs_inmem_tuplist.head;
+	dlist_iter	iter;
+
+	/* scan->orderByData is never used in index scans over catalog tables */
+	Assert(scan->numberOfOrderBys == 0);
+	Assert(scan->numberOfKeys >= 0 && scan->numberOfKeys <= FasttabIndexMaxAttributes);
+
+	if (!fasttab_index_tuple_matches_where_condition(scan, tup))
+		return false;
+
+	/* Using regular transaction memory context here. */
+	dlist_tup = palloc(sizeof(DListHeapTupleData));
+	dlist_tup->tup = heap_copytuple(tup);
+
+	dlist_foreach(iter, &scan->xs_inmem_tuplist)
+	{
+		DListHeapTuple dlist_curr = (DListHeapTuple) iter.cur;
+
+		if (fasttab_index_compare_tuples(dlist_curr->tup, tup, scan) >= 0)
+			break;
+
+		insert_after = iter.cur;
+	}
+
+	dlist_insert_after(insert_after, &dlist_tup->node);
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_insert_tuple_in_sorted_list scan = %p, tup oid = %d, tuple added to list",
+		 scan, HeapTupleGetOid(tup));
+#endif
+
+	return true;
+}
+
+/*
+ * Initialize part of `scan` related to virtual catalog. Basically
+ * index_beginscan implementation for virtual tuples.
+ *
+ * NB: scan->keyData is not initialized here (usually filled with 0x7f's)
+ */
+void
+fasttab_index_beginscan(IndexScanDesc scan)
+{
+	Oid			indexId = RelationGetRelid(scan->indexRelation);
+
+	Assert(PointerIsValid(scan->indexRelation));
+
+	if (!IsFasttabHandledIndexId(indexId))
+		return;
+
+	scan->xs_regular_tuple_enqueued = false;
+	scan->xs_regular_scan_finished = false;
+	scan->xs_scan_finish_returned = false;
+
+	/* indexMethods is accessed quite often so we memoize it */
+	scan->indexMethods = GetFasttabIndexMethods(indexId);
+
+	/*
+	 * xs_inmem_tuplist is initialized when fasttab_index_getnext_tid_merge is
+	 * called first time. We are not doing it here because:
+	 *
+	 * 1) It's more efficient this way, since sometimes beginscan/rescan are
+	 * called without any actual scanning
+	 *
+	 * 2) Sometimes `scan` passed to beginscan is not fully initilized so we
+	 * can't filter tuples by WHERE condition here
+	 *
+	 * 3) We would like to filter tuples by WHERE condition ASAP, otherwise
+	 * memory will be wasted on tuples that will be filtered anyway
+	 */
+	scan->xs_inmem_tuplist_init_done = false;
+	dlist_init(&scan->xs_inmem_tuplist);
+
+	/*
+	 * Make sure scan->xs_ctup.t_self has proper initial value (required in
+	 * index_getnext_tid)
+	 */
+	ItemPointerSetInvalid(&scan->xs_ctup.t_self);
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_beginscan (could be called from rescan), scan = %p, indexId = %u "
+		 "scan->numberOfKeys = %d, scan->keyData = %p, scan->numberOfOrderBys = %d, scan->orderByData = %p",
+	scan, indexId, scan->numberOfKeys, scan->keyData, scan->numberOfOrderBys,
+		 scan->orderByData
+		);
+#endif
+
+}
+
+/*
+ * Free part of `scan` related to virtual catalog. Basically index_endscan
+ * implementation for virtual tuples.
+ */
+void
+fasttab_index_endscan(IndexScanDesc scan)
+{
+	Assert(PointerIsValid(scan->indexRelation));
+
+	if (!IsFasttabHandledIndexId(RelationGetRelid(scan->indexRelation)))
+		return;
+
+	/* Free in-memory tuples left. */
+	FasttabDListFree(&scan->xs_inmem_tuplist);
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_endscan (could be called from rescan), scan = %p, scan->indexRelation relid = %u",
+		 scan, RelationGetRelid(scan->indexRelation)
+		);
+#endif
+
+}
+
+/*
+ * Reinitialize part of `scan` related to virtual catalog. Basically
+ * index_rescan implementation for virtual tuples.
+ *
+ * NB: scan->keyData is not initialized here (usually filled with 0x7f's)
+ */
+void
+fasttab_index_rescan(IndexScanDesc scan, ScanKey keys, int nkeys,
+					 ScanKey orderbys, int norderbys)
+{
+	fasttab_index_endscan(scan);
+	fasttab_index_beginscan(scan);
+}
+
+/*
+ * Fetch virtual or regular tuple from heap. Almost as heap_fetch, but also
+ * understands HOT chains.
+ *
+ * Returns true if tuple was found, false otherwise.
+ */
+bool
+fasttab_simple_heap_fetch(Relation relation, Snapshot snapshot,
+						  HeapTuple tuple)
+{
+	Page		page;
+	bool		found;
+	Buffer		buffer = InvalidBuffer;
+	ItemPointer tid = &(tuple->t_self);
+
+	/*
+	 * No need to lock any buffers for in-memory tuple, they could not even
+	 * exist!
+	 */
+	if (IsFasttabItemPointer(tid))
+		return heap_hot_search_buffer(tid, relation, buffer, snapshot, tuple, NULL, true);
+
+	/* Fetch and pin the appropriate page of the relation. */
+	buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(tid));
+
+	/* Need share lock on buffer to examine tuple commit status. */
+	LockBuffer(buffer, BUFFER_LOCK_SHARE);
+	page = BufferGetPage(buffer);
+	TestForOldSnapshot(snapshot, relation, page);
+
+	found = heap_hot_search_buffer(tid, relation, buffer, snapshot, tuple,
+								   NULL, true);
+
+	LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
+	ReleaseBuffer(buffer);
+
+	return found;
+}
+
+/*
+ * Make sure scan->xs_inmem_tuplist is initialized.
+ */
+static void
+fasttab_index_make_sure_inmem_tuplist_init_done(IndexScanDesc scan)
+{
+	FasttabSnapshot fasttab_snapshot;
+	dlist_iter	iter;
+	int			idx;
+
+	Assert(PointerIsValid(scan->indexRelation));
+
+	/* initialize scan->xs_inmem_tuplist during first call */
+	if (scan->xs_inmem_tuplist_init_done)
+		return;
+
+	idx = GetSnapshotRelationIdxByOid(RelationGetRelid(scan->heapRelation));
+
+	fasttab_snapshot = FasttabSnapshotGetCurrent();
+	dlist_foreach(iter, &fasttab_snapshot->relationData[idx].tuples)
+	{
+		DListHeapTuple dlist_curr = (DListHeapTuple) iter.cur;
+
+		(void) fasttab_index_insert_tuple_in_sorted_list(scan, dlist_curr->tup);
+	}
+
+	scan->xs_inmem_tuplist_init_done = true;
+}
+
+/*
+ * Get next virtual or regular TID from a scan. Basically a wrapper around
+ * indexRelation->rd_amroutine->amgettuple procedure.
+ *
+ * NB: we filter tuples using scan->keyData _here_ since keyData is not always
+ * initialized when fasttab_index_beginscan or _rescan are called (usually
+ * filled with 0x7f's)
+ */
+bool
+fasttab_index_getnext_tid_merge(IndexScanDesc scan, ScanDirection direction)
+{
+	bool		fetched;
+	DListHeapTuple ret_node;
+
+	Assert(PointerIsValid(scan->indexRelation));
+
+	if (!IsFasttabHandledIndexId(RelationGetRelid(scan->indexRelation)))
+	{
+		/*
+		 * Regular logic.
+		 *
+		 * The AM's amgettuple proc finds the next index entry matching the
+		 * scan keys, and puts the TID into scan->xs_ctup.t_self.  It should
+		 * also set scan->xs_recheck and possibly scan->xs_itup, though we pay
+		 * no attention to those fields here.
+		 */
+		return scan->indexRelation->rd_amroutine->amgettuple(scan, direction);
+	}
+
+	/* Initialize scan->xs_inmem_tuplist during first call. */
+	fasttab_index_make_sure_inmem_tuplist_init_done(scan);
+
+	if (dlist_is_empty(&scan->xs_inmem_tuplist))		/* in-memory tuples
+														 * enumiration is over? */
+	{
+#ifdef FASTTAB_DEBUG
+		elog(NOTICE, "FASTTAB: fasttab_index_getnext_tid_merge, scan = %p, fake tuples list is empty, xs_regular_scan_finished = %u, xs_scan_finish_returned = %u",
+		scan, scan->xs_regular_scan_finished, scan->xs_scan_finish_returned);
+#endif
+
+		/*
+		 * If ->amgettuple() already returned false we should not call it once
+		 * again.  In this case btree index will start a scan all over again,
+		 * see btgettuple implementation.  Still if user will call this
+		 * procedure once again dispite of returned 'false' value she probably
+		 * knows what she is doing.
+		 */
+		if (scan->xs_regular_scan_finished && (!scan->xs_scan_finish_returned))
+		{
+			scan->xs_scan_finish_returned = true;
+			return false;
+		}
+
+		/* regular logic */
+		return scan->indexRelation->rd_amroutine->amgettuple(scan, direction);
+	}
+
+	/*
+	 * Other directions are not used in index-only scans for catalog tables.
+	 * No need to check direction above this point since only here
+	 * scan->xs_inmem_tuplist is both initialized and non-empty.
+	 */
+	Assert(ScanDirectionIsForward(direction));
+
+	/* If there is no regular tuple in in-memory queue, we should load one. */
+	while ((!scan->xs_regular_tuple_enqueued) && (!scan->xs_regular_scan_finished))
+	{
+		if (scan->indexRelation->rd_amroutine->amgettuple(scan, direction))
+		{
+			HeapTupleData regular_tup;
+
+			regular_tup.t_self = scan->xs_ctup.t_self;
+			fetched = fasttab_simple_heap_fetch(scan->heapRelation, scan->xs_snapshot,
+												&regular_tup);
+
+			if (!fetched)
+			{
+#ifdef FASTTAB_DEBUG
+				elog(NOTICE, "FASTTAB: fasttab_index_getnext_tid_merge, scan = %p, indexed tuple not found, 'continue;'",
+					 scan);
+#endif
+				continue;
+			}
+			scan->xs_regular_tuple_enqueued = fasttab_index_insert_tuple_in_sorted_list(scan, &regular_tup);
+		}
+		else
+			scan->xs_regular_scan_finished = true;
+	}
+
+	Assert(scan->xs_regular_scan_finished || scan->xs_regular_tuple_enqueued);
+
+	ret_node = (DListHeapTuple) dlist_pop_head_node(&scan->xs_inmem_tuplist);
+	Assert(PointerIsValid(ret_node));
+
+	scan->xs_recheck = false;
+	ItemPointerCopy(&ret_node->tup->t_self, &scan->xs_ctup.t_self);
+
+	if (!IsFasttabItemPointer(&scan->xs_ctup.t_self))
+		scan->xs_regular_tuple_enqueued = false;
+
+#ifdef FASTTAB_DEBUG
+	elog(NOTICE, "FASTTAB: fasttab_index_getnext_tid_merge, scan = %p, direction = %d, scan->indexRelation relid = %u, return tuple tid = %08X/%04X",
+		 scan, direction, RelationGetRelid(scan->indexRelation),
+		 BlockIdGetBlockNumber(&scan->xs_ctup.t_self.ip_blkid),
+		 scan->xs_ctup.t_self.ip_posid
+		);
+#endif
+
+	scan->xs_itup = fasttab_index_form_tuple(ret_node->tup, scan);
+	DListHeapTupleFree(ret_node);
+	return true;
+}
+
+/*
+ * Get all tuples, virtual and regular, at once from an index scan. Basically
+ * index_getbitmap implementation for virtual tuples.
+ *
+ * Returns true on success and false if relation doesn't have a virtual part.
+ */
+bool
+fasttab_index_getbitmap(IndexScanDesc scan, TIDBitmap *bitmap, int64 *result)
+{
+	int64		ntids = 0;
+	bool		heap_opened = false;
+
+	Assert(PointerIsValid(scan->indexRelation));
+
+	if (!IsFasttabHandledIndexId(RelationGetRelid(scan->indexRelation)))
+		return false;
+
+	/* Fill heapRelation if it's NULL, we require it in fasttab_* procedures */
+	if (!scan->heapRelation)
+	{
+		scan->heapRelation = heap_open(scan->indexRelation->rd_index->indrelid,
+									   NoLock);
+		heap_opened = true;
+	}
+
+	/* Initialize scan->xs_inmem_tuplist during first call. */
+	fasttab_index_make_sure_inmem_tuplist_init_done(scan);
+
+	/* There are in fact no in-memory tuples? */
+	if (dlist_is_empty(&scan->xs_inmem_tuplist))
+	{
+		if (heap_opened)		/* cleanup */
+		{
+			heap_close(scan->heapRelation, NoLock);
+			scan->heapRelation = NULL;
+		}
+		return false;
+	}
+
+	while (fasttab_index_getnext_tid_merge(scan, ForwardScanDirection))
+	{
+		tbm_add_tuples(bitmap, &scan->xs_ctup.t_self, 1, false);
+		ntids++;
+	}
+
+	if (heap_opened)			/* cleanup */
+	{
+		heap_close(scan->heapRelation, NoLock);
+		scan->heapRelation = NULL;
+	}
+
+	*result = ntids;
+	return true;
+}
+
+
+/*****************************************************************************
+			   PROCEDURES USED IN FasttabRelationMethodsTable
+ *****************************************************************************/
+
+/*
+ * Determine wheter given tuple of pg_class relation should be stored in-memory.
+ *
+ * If tuple's relpersistence = RELPERSISTENCE_FAST_TEMP it should be virtual.
+ */
+static bool
+pg_class_is_inmem_tuple(Relation relation, HeapTuple tup,
+						FasttabSnapshot fasttab_snapshot, int tableIdx)
+{
+	bool		isnull;
+	Datum		relpersistencedat;
+	TupleDesc	tupledesc;
+
+	Assert(RelationGetRelid(relation) == RelationRelationId);
+
+	tupledesc = RelationGetDescr(relation);
+	relpersistencedat = heap_getattr(tup, Anum_pg_class_relpersistence,
+									 tupledesc, &isnull);
+	Assert(!isnull);
+	return ((char) relpersistencedat == RELPERSISTENCE_FAST_TEMP);
+}
+
+/*
+ * Determine wheter given tuple of relations other than pg_class should be
+ * stored in-memory.
+ *
+ * If tuple references to virtual pg_class tuple it should be virtual as well.
+ */
+static bool
+generic_is_inmem_tuple(Relation relation, HeapTuple tup,
+					   FasttabSnapshot fasttab_snapshot, int tableIdx)
+{
+	dlist_iter	iter;
+	TupleDesc	tupledesc;
+	Oid			values[FasttabRelationMaxOidAttributes];
+	bool		isnull;
+	int			i,
+				pg_class_idx,
+				noidattr = FasttabRelationMethodsTable[tableIdx].noidattr;
+
+	Assert(IsFasttabHandledRelationId(RelationGetRelid(relation)));
+	Assert(tableIdx >= 0 && tableIdx < FasttabSnapshotTablesNumber);
+	Assert(noidattr > 0 && noidattr <= FasttabRelationMaxOidAttributes);
+
+	/*
+	 * Special case. During table creation pg_type and pg_depend are modified
+	 * before pg_class (see heap_create_with_catalog implementation) so there
+	 * is no way to tell wheter tuples are in-memory without using
+	 * relperistence hint. Also this check could be considered as an
+	 * optimization.
+	 */
+	if ((RelationGetRelid(relation) == TypeRelationId) || (RelationGetRelid(relation) == DependRelationId))
+		return (CurrentRelpersistenceHint == RELPERSISTENCE_FAST_TEMP);
+
+	tupledesc = RelationGetDescr(relation);
+
+	for (i = 0; i < noidattr; i++)
+	{
+		values[i] = DatumGetObjectId(heap_getattr(tup,
+						FasttabRelationMethodsTable[tableIdx].attrNumbers[i],
+												  tupledesc, &isnull));
+		Assert(!isnull);
+	}
+
+	/*
+	 * Check whether there is an in-memory pg_class tuple with oid from
+	 * values[] array
+	 */
+	pg_class_idx = GetSnapshotRelationIdxByOid(RelationRelationId);
+	dlist_foreach(iter, &fasttab_snapshot->relationData[pg_class_idx].tuples)
+	{
+		DListHeapTuple dlist_tup = (DListHeapTuple) iter.cur;
+		Oid			oid = HeapTupleGetOid(dlist_tup->tup);
+
+		for (i = 0; i < noidattr; i++)
+		{
+			if (oid == values[i])
+				return true;
+		}
+	}
+
+	return false;
+}
+
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index fac166d..d952512 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -846,7 +846,8 @@ gistGetFakeLSN(Relation rel)
 {
 	static XLogRecPtr counter = 1;
 
-	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		rel->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP)
 	{
 		/*
 		 * Temporary relations are only accessible in our session, so a simple
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 38bba16..835e610 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -53,6 +53,7 @@
 #include "access/xlog.h"
 #include "access/xloginsert.h"
 #include "access/xlogutils.h"
+#include "access/fasttab.h"
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "miscadmin.h"
@@ -72,6 +73,8 @@
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 #include "utils/tqual.h"
+#include "utils/memutils.h"
+#include "lib/ilist.h"
 
 
 /* GUC variable */
@@ -1506,6 +1509,9 @@ heap_beginscan_internal(Relation relation, Snapshot snapshot,
 
 	initscan(scan, key, false);
 
+	/* Initialize part of `scan` related to virtual catalog. */
+	fasttab_beginscan(scan);
+
 	return scan;
 }
 
@@ -1546,6 +1552,9 @@ heap_rescan(HeapScanDesc scan,
 		parallel_scan->phs_cblock = parallel_scan->phs_startblock;
 		SpinLockRelease(&parallel_scan->phs_mutex);
 	}
+
+	/* Reinitialize part of `scan` related to virtual catalog. */
+	fasttab_beginscan(scan);
 }
 
 /* ----------------
@@ -1779,6 +1788,13 @@ retry:
 HeapTuple
 heap_getnext(HeapScanDesc scan, ScanDirection direction)
 {
+	HeapTuple	fasttab_result;
+
+	/* First return all virtual tuples, then regular ones. */
+	fasttab_result = fasttab_getnext(scan, direction);
+	if (HeapTupleIsValid(fasttab_result))
+		return fasttab_result;
+
 	/* Note: no locking manipulations needed */
 
 	HEAPDEBUG_1;				/* heap_getnext( info ) */
@@ -1859,6 +1875,8 @@ heap_fetch(Relation relation,
 	OffsetNumber offnum;
 	bool		valid;
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	/*
 	 * Fetch and pin the appropriate page of the relation.
 	 */
@@ -1984,12 +2002,23 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
 					   Snapshot snapshot, HeapTuple heapTuple,
 					   bool *all_dead, bool first_call)
 {
-	Page		dp = (Page) BufferGetPage(buffer);
+	Page		dp;
 	TransactionId prev_xmax = InvalidTransactionId;
 	OffsetNumber offnum;
 	bool		at_chain_start;
 	bool		valid;
 	bool		skip;
+	bool		fasttab_result;
+
+	/* Return matching virtual tuple if there is one. */
+	if (fasttab_hot_search_buffer(tid, relation, heapTuple, all_dead, &fasttab_result))
+		return fasttab_result;
+
+	/*
+	 * `buffer` can be InvalidBuffer for in-memory tuples, so we should call
+	 * BufferGetPage only after we verified it's not a case.
+	 */
+	dp = (Page) BufferGetPage(buffer);
 
 	/* If this is not the first call, previous call returned a (live!) tuple */
 	if (all_dead)
@@ -2158,6 +2187,8 @@ heap_get_latest_tid(Relation relation,
 	ItemPointerData ctid;
 	TransactionId priorXmax;
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	/* this is to avoid Assert failures on bad input */
 	if (!ItemPointerIsValid(tid))
 		return;
@@ -2376,6 +2407,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
 	Buffer		buffer;
 	Buffer		vmbuffer = InvalidBuffer;
 	bool		all_visible_cleared = false;
+	Oid			fasttab_result;
 
 	/*
 	 * Fill in tuple header fields, assign an OID, and toast the tuple if
@@ -2386,6 +2418,10 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
 	 */
 	heaptup = heap_prepare_insert(relation, tup, xid, cid, options);
 
+	/* If it's a virtual tuple it should be inserted in virtual catalog. */
+	if (fasttab_insert(relation, tup, heaptup, &fasttab_result))
+		return fasttab_result;
+
 	/*
 	 * Find buffer to insert this tuple into.  If the page is all visible,
 	 * this will also pin the requisite visibility map page.
@@ -2644,6 +2680,8 @@ heap_multi_insert(Relation relation, HeapTuple *tuples, int ntuples,
 	bool		need_tuple_data = RelationIsLogicallyLogged(relation);
 	bool		need_cids = RelationIsAccessibleInLogicalDecoding(relation);
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	needwal = !(options & HEAP_INSERT_SKIP_WAL) && RelationNeedsWAL(relation);
 	saveFreeSpace = RelationGetTargetPageFreeSpace(relation,
 												   HEAP_DEFAULT_FILLFACTOR);
@@ -3006,6 +3044,10 @@ heap_delete(Relation relation, ItemPointer tid,
 
 	Assert(ItemPointerIsValid(tid));
 
+	/* If it's a virtual tuple, it should be deleted from virtual catalog. */
+	if (fasttab_delete(relation, tid))
+		return HeapTupleMayBeUpdated;
+
 	/*
 	 * Forbid this during a parallel operation, lest it allocate a combocid.
 	 * Other workers might need that combocid for visibility checks, and we
@@ -3488,6 +3530,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 				(errcode(ERRCODE_INVALID_TRANSACTION_STATE),
 				 errmsg("cannot update tuples during a parallel operation")));
 
+	/* If it's a virtual tuple it should be updated in virtual catalog. */
+	if (fasttab_update(relation, otid, newtup))
+		return HeapTupleMayBeUpdated;
+
 	/*
 	 * Fetch the list of attributes to be checked for HOT update.  This is
 	 * wasted effort if we fail to update or have to put the new tuple on a
@@ -4581,6 +4627,8 @@ heap_lock_tuple(Relation relation, HeapTuple tuple,
 	bool		have_tuple_lock = false;
 	bool		cleared_all_frozen = false;
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	*buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(tid));
 	block = ItemPointerGetBlockNumber(tid);
 
@@ -5212,6 +5260,8 @@ static bool
 heap_acquire_tuplock(Relation relation, ItemPointer tid, LockTupleMode mode,
 					 LockWaitPolicy wait_policy, bool *have_tuple_lock)
 {
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	if (*have_tuple_lock)
 		return true;
 
@@ -5904,6 +5954,8 @@ static HTSU_Result
 heap_lock_updated_tuple(Relation rel, HeapTuple tuple, ItemPointer ctid,
 						TransactionId xid, LockTupleMode mode)
 {
+	Assert(!IsFasttabHandledRelationId(rel->rd_id));
+
 	if (!ItemPointerEquals(&tuple->t_self, ctid))
 	{
 		/*
@@ -5949,6 +6001,8 @@ heap_finish_speculative(Relation relation, HeapTuple tuple)
 	ItemId		lp = NULL;
 	HeapTupleHeader htup;
 
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
+
 	buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(&(tuple->t_self)));
 	LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE);
 	page = (Page) BufferGetPage(buffer);
@@ -6042,6 +6096,7 @@ heap_abort_speculative(Relation relation, HeapTuple tuple)
 	Buffer		buffer;
 
 	Assert(ItemPointerIsValid(tid));
+	Assert(!IsFasttabHandledRelationId(relation->rd_id));
 
 	block = ItemPointerGetBlockNumber(tid);
 	buffer = ReadBuffer(relation, block);
@@ -6179,6 +6234,10 @@ heap_inplace_update(Relation relation, HeapTuple tuple)
 	uint32		oldlen;
 	uint32		newlen;
 
+	/* If it's a virtual tuple it should be updated in virtual catalog. */
+	if (fasttab_inplace_update(relation, tuple))
+		return;
+
 	/*
 	 * For now, parallel operations are required to be strictly read-only.
 	 * Unlike a regular update, this should never create a combo CID, so it
@@ -6553,6 +6612,8 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
 	TransactionId xid;
 	bool		totally_frozen = true;
 
+	Assert(!IsFasttabItemPointer(&tuple->t_ctid));
+
 	frz->frzflags = 0;
 	frz->t_infomask2 = tuple->t_infomask2;
 	frz->t_infomask = tuple->t_infomask;
@@ -6723,6 +6784,8 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
 void
 heap_execute_freeze_tuple(HeapTupleHeader tuple, xl_heap_freeze_tuple *frz)
 {
+	Assert(!IsFasttabItemPointer(&tuple->t_ctid));
+
 	HeapTupleHeaderSetXmax(tuple, frz->xmax);
 
 	if (frz->frzflags & XLH_FREEZE_XVAC)
@@ -7125,6 +7188,8 @@ heap_tuple_needs_eventual_freeze(HeapTupleHeader tuple)
 {
 	TransactionId xid;
 
+	Assert(!IsFasttabItemPointer(&tuple->t_ctid));
+
 	/*
 	 * If xmin is a normal transaction ID, this tuple is definitely not
 	 * frozen.
@@ -7179,6 +7244,8 @@ heap_tuple_needs_freeze(HeapTupleHeader tuple, TransactionId cutoff_xid,
 {
 	TransactionId xid;
 
+	Assert(!IsFasttabItemPointer(&tuple->t_ctid));
+
 	xid = HeapTupleHeaderGetXmin(tuple);
 	if (TransactionIdIsNormal(xid) &&
 		TransactionIdPrecedes(xid, cutoff_xid))
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 54b71cb..7678560 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -69,6 +69,7 @@
 #include "access/relscan.h"
 #include "access/transam.h"
 #include "access/xlog.h"
+#include "access/fasttab.h"
 #include "catalog/catalog.h"
 #include "catalog/index.h"
 #include "pgstat.h"
@@ -193,9 +194,15 @@ index_insert(Relation indexRelation,
 			 Relation heapRelation,
 			 IndexUniqueCheck checkUnique)
 {
+	bool		result;
+
 	RELATION_CHECKS;
 	CHECK_REL_PROCEDURE(aminsert);
 
+	/* If it's a virtual ItemPointer, process it accordingly. */
+	if (fasttab_index_insert(indexRelation, heap_t_ctid, &result))
+		return result;
+
 	if (!(indexRelation->rd_amroutine->ampredlocks))
 		CheckForSerializableConflictIn(indexRelation,
 									   (HeapTuple) NULL,
@@ -262,6 +269,7 @@ static IndexScanDesc
 index_beginscan_internal(Relation indexRelation,
 						 int nkeys, int norderbys, Snapshot snapshot)
 {
+	IndexScanDesc scan;
 	RELATION_CHECKS;
 	CHECK_REL_PROCEDURE(ambeginscan);
 
@@ -276,8 +284,12 @@ index_beginscan_internal(Relation indexRelation,
 	/*
 	 * Tell the AM to open a scan.
 	 */
-	return indexRelation->rd_amroutine->ambeginscan(indexRelation, nkeys,
-													norderbys);
+	scan = indexRelation->rd_amroutine->ambeginscan(indexRelation, nkeys,
+													  norderbys);
+	/* Initialize part of `scan` related to virtual catalog */
+	fasttab_index_beginscan(scan);
+
+	return scan;
 }
 
 /* ----------------
@@ -316,6 +328,9 @@ index_rescan(IndexScanDesc scan,
 
 	scan->indexRelation->rd_amroutine->amrescan(scan, keys, nkeys,
 												orderbys, norderbys);
+
+	/* Reinitialize part of `scan` related to virtual catalog. */
+	fasttab_index_rescan(scan, keys, nkeys, orderbys, norderbys);
 }
 
 /* ----------------
@@ -328,6 +343,9 @@ index_endscan(IndexScanDesc scan)
 	SCAN_CHECKS;
 	CHECK_SCAN_PROCEDURE(amendscan);
 
+	/* Free part of `scan` related to virtual catalog. */
+	fasttab_index_endscan(scan);
+
 	/* Release any held pin on a heap page */
 	if (BufferIsValid(scan->xs_cbuf))
 	{
@@ -378,6 +396,7 @@ void
 index_restrpos(IndexScanDesc scan)
 {
 	Assert(IsMVCCSnapshot(scan->xs_snapshot));
+	Assert(!IsFasttabHandledIndexId(scan->indexRelation->rd_id));
 
 	SCAN_CHECKS;
 	CHECK_SCAN_PROCEDURE(amrestrpos);
@@ -406,13 +425,8 @@ index_getnext_tid(IndexScanDesc scan, ScanDirection direction)
 
 	Assert(TransactionIdIsValid(RecentGlobalXmin));
 
-	/*
-	 * The AM's amgettuple proc finds the next index entry matching the scan
-	 * keys, and puts the TID into scan->xs_ctup.t_self.  It should also set
-	 * scan->xs_recheck and possibly scan->xs_itup, though we pay no attention
-	 * to those fields here.
-	 */
-	found = scan->indexRelation->rd_amroutine->amgettuple(scan, direction);
+	/* Get the next regular or virtual TID */
+	found = fasttab_index_getnext_tid_merge(scan, direction);
 
 	/* Reset kill flag immediately for safety */
 	scan->kill_prior_tuple = false;
@@ -460,6 +474,17 @@ index_fetch_heap(IndexScanDesc scan)
 	bool		all_dead = false;
 	bool		got_heap_tuple;
 
+	/* Is it a virtual TID? */
+	if (IsFasttabItemPointer(tid))
+	{
+		bool		fasttab_result;
+
+		/* Just get virtual tuple by TID */
+		got_heap_tuple = fasttab_hot_search_buffer(tid, scan->heapRelation, &scan->xs_ctup, &all_dead, &fasttab_result);
+		Assert(got_heap_tuple && fasttab_result);
+		return &scan->xs_ctup;
+	}
+
 	/* We can skip the buffer-switching logic if we're in mid-HOT chain. */
 	if (!scan->xs_continue_hot)
 	{
@@ -594,10 +619,10 @@ index_getbitmap(IndexScanDesc scan, TIDBitmap *bitmap)
 	/* just make sure this is false... */
 	scan->kill_prior_tuple = false;
 
-	/*
-	 * have the am's getbitmap proc do all the work.
-	 */
-	ntids = scan->indexRelation->rd_amroutine->amgetbitmap(scan, bitmap);
+	/* Try to use virtual catalog procedure first */
+	if (!fasttab_index_getbitmap(scan, bitmap, &ntids))
+		/* if it failed - have the am's getbitmap proc do all the work. */
+		ntids = scan->indexRelation->rd_amroutine->amgetbitmap(scan, bitmap);
 
 	pgstat_count_index_tuples(scan->indexRelation, ntids);
 
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index ef69290..b081165 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -19,6 +19,7 @@
 #include "access/nbtree.h"
 #include "access/transam.h"
 #include "access/xloginsert.h"
+#include "access/fasttab.h"
 #include "miscadmin.h"
 #include "storage/lmgr.h"
 #include "storage/predicate.h"
@@ -330,6 +331,13 @@ _bt_check_unique(Relation rel, IndexTuple itup, Relation heapRel,
 					TransactionId xwait;
 
 					/*
+					 * If its in-memory tuple there is for sure no transaction
+					 * to wait for.
+					 */
+					if (IsFasttabItemPointer(&htid))
+						return InvalidTransactionId;
+
+					/*
 					 * It is a duplicate. If we are only doing a partial
 					 * check, then don't bother checking if the tuple is being
 					 * updated in another transaction. Just return the fact
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 23f36ea..c7dc356 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -30,6 +30,7 @@
 #include "access/xlog.h"
 #include "access/xloginsert.h"
 #include "access/xlogutils.h"
+#include "access/fasttab.h"
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/storage.h"
@@ -1928,6 +1929,9 @@ StartTransaction(void)
 	s->state = TRANS_INPROGRESS;
 
 	ShowTransactionState("StartTransaction");
+
+	/* Make preparations related to virtual catalog */
+	fasttab_begin_transaction();
 }
 
 
@@ -2165,6 +2169,9 @@ CommitTransaction(void)
 	 */
 	s->state = TRANS_DEFAULT;
 
+	/* Perform actions related to virtual catalog. */
+	fasttab_end_transaction();
+
 	RESUME_INTERRUPTS();
 }
 
@@ -2611,6 +2618,9 @@ AbortTransaction(void)
 		pgstat_report_xact_timestamp(0);
 	}
 
+	/* Perform actions related to virtual catalog. */
+	fasttab_abort_transaction();
+
 	/*
 	 * State remains TRANS_ABORT until CleanupTransaction().
 	 */
@@ -3795,6 +3805,9 @@ DefineSavepoint(char *name)
 				 BlockStateAsString(s->blockState));
 			break;
 	}
+
+	/* Perform actions related to virtual catalog. */
+	fasttab_define_savepoint(name);
 }
 
 /*
@@ -4034,6 +4047,9 @@ RollbackToSavepoint(List *options)
 	else
 		elog(FATAL, "RollbackToSavepoint: unexpected state %s",
 			 BlockStateAsString(xact->blockState));
+
+	/* Perform actions related to virtual catalog. */
+	fasttab_rollback_to_savepoint(name);
 }
 
 /*
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 1baaa0b..76b9d4c 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -390,6 +390,7 @@ GetNewRelFileNode(Oid reltablespace, Relation pg_class, char relpersistence)
 	switch (relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			backend = BackendIdForTempRelations();
 			break;
 		case RELPERSISTENCE_UNLOGGED:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 04d7840..9bbfc77 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -16,6 +16,7 @@
 
 #include "access/htup_details.h"
 #include "access/xact.h"
+#include "access/fasttab.h"
 #include "catalog/dependency.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
@@ -583,6 +584,13 @@ findDependentObjects(const ObjectAddress *object,
 	{
 		Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(tup);
 
+		/*
+		 * Ignore in-memory tuples here. They are properly handled by virtual
+		 * catalog logic already.
+		 */
+		if (IsFasttabItemPointer(&tup->t_self))
+			continue;
+
 		otherObject.classId = foundDep->refclassid;
 		otherObject.objectId = foundDep->refobjid;
 		otherObject.objectSubId = foundDep->refobjsubid;
@@ -760,6 +768,13 @@ findDependentObjects(const ObjectAddress *object,
 		Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(tup);
 		int			subflags;
 
+		/*
+		 * Ignore in-memory tuples here. They are properly handled by virtual
+		 * catalog logic already.
+		 */
+		if (IsFasttabItemPointer(&tup->t_self))
+			continue;
+
 		otherObject.classId = foundDep->classid;
 		otherObject.objectId = foundDep->objid;
 		otherObject.objectSubId = foundDep->objsubid;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index e997b57..b33eb6d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -35,6 +35,7 @@
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xlog.h"
+#include "access/fasttab.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
@@ -996,7 +997,7 @@ AddNewRelationType(const char *typeName,
  *	tupdesc: tuple descriptor (source of column definitions)
  *	cooked_constraints: list of precooked check constraints and defaults
  *	relkind: relkind for new rel
- *	relpersistence: rel's persistence status (permanent, temp, or unlogged)
+ *	relpersistence: rel's persistence status (permanent, temp, fast temp or unlogged)
  *	shared_relation: TRUE if it's to be a shared relation
  *	mapped_relation: TRUE if the relation will use the relfilenode map
  *	oidislocal: TRUE if oid column (if any) should be marked attislocal
@@ -1184,70 +1185,95 @@ heap_create_with_catalog(const char *relname,
 							  relkind == RELKIND_COMPOSITE_TYPE))
 		new_array_oid = AssignTypeArrayOid();
 
-	/*
-	 * Since defining a relation also defines a complex type, we add a new
-	 * system type corresponding to the new relation.  The OID of the type can
-	 * be preselected by the caller, but if reltypeid is InvalidOid, we'll
-	 * generate a new OID for it.
-	 *
-	 * NOTE: we could get a unique-index failure here, in case someone else is
-	 * creating the same type name in parallel but hadn't committed yet when
-	 * we checked for a duplicate name above.
-	 */
-	new_type_addr = AddNewRelationType(relname,
-									   relnamespace,
-									   relid,
-									   relkind,
-									   ownerid,
-									   reltypeid,
-									   new_array_oid);
-	new_type_oid = new_type_addr.objectId;
-	if (typaddress)
-		*typaddress = new_type_addr;
-
-	/*
-	 * Now make the array type if wanted.
-	 */
-	if (OidIsValid(new_array_oid))
+	PG_TRY();
 	{
-		char	   *relarrayname;
+		/* Set a relpersistence hint. See procedure description. */
+		fasttab_set_relpersistence_hint(relpersistence);
 
-		relarrayname = makeArrayTypeName(relname, relnamespace);
+		/*
+		 * Since defining a relation also defines a complex type, we add a new
+		 * system type corresponding to the new relation.  The OID of the type
+		 * can be preselected by the caller, but if reltypeid is InvalidOid,
+		 * we'll generate a new OID for it.
+		 *
+		 * NOTE: we could get a unique-index failure here, in case someone
+		 * else is creating the same type name in parallel but hadn't
+		 * committed yet when we checked for a duplicate name above.
+		 */
+		new_type_addr = AddNewRelationType(relname,
+										   relnamespace,
+										   relid,
+										   relkind,
+										   ownerid,
+										   reltypeid,
+										   new_array_oid);
 
-		TypeCreate(new_array_oid,		/* force the type's OID to this */
-				   relarrayname,	/* Array type name */
-				   relnamespace,	/* Same namespace as parent */
-				   InvalidOid,	/* Not composite, no relationOid */
-				   0,			/* relkind, also N/A here */
-				   ownerid,		/* owner's ID */
-				   -1,			/* Internal size (varlena) */
-				   TYPTYPE_BASE,	/* Not composite - typelem is */
-				   TYPCATEGORY_ARRAY,	/* type-category (array) */
-				   false,		/* array types are never preferred */
-				   DEFAULT_TYPDELIM,	/* default array delimiter */
-				   F_ARRAY_IN,	/* array input proc */
-				   F_ARRAY_OUT, /* array output proc */
-				   F_ARRAY_RECV,	/* array recv (bin) proc */
-				   F_ARRAY_SEND,	/* array send (bin) proc */
-				   InvalidOid,	/* typmodin procedure - none */
-				   InvalidOid,	/* typmodout procedure - none */
-				   F_ARRAY_TYPANALYZE,	/* array analyze procedure */
-				   new_type_oid,	/* array element type - the rowtype */
-				   true,		/* yes, this is an array type */
-				   InvalidOid,	/* this has no array type */
-				   InvalidOid,	/* domain base type - irrelevant */
-				   NULL,		/* default value - none */
-				   NULL,		/* default binary representation */
-				   false,		/* passed by reference */
-				   'd',			/* alignment - must be the largest! */
-				   'x',			/* fully TOASTable */
-				   -1,			/* typmod */
-				   0,			/* array dimensions for typBaseType */
-				   false,		/* Type NOT NULL */
-				   InvalidOid); /* rowtypes never have a collation */
+		/* Clear relpersistence hint. */
+		fasttab_clear_relpersistence_hint();
+
+		new_type_oid = new_type_addr.objectId;
+		if (typaddress)
+			*typaddress = new_type_addr;
 
-		pfree(relarrayname);
+		/*
+		 * Now make the array type if wanted.
+		 */
+		if (OidIsValid(new_array_oid))
+		{
+			char	   *relarrayname;
+
+			relarrayname = makeArrayTypeName(relname, relnamespace);
+
+			/* Set a relpersistence hint. See procedure description. */
+			fasttab_set_relpersistence_hint(relpersistence);
+
+			TypeCreate(new_array_oid,	/* force the type's OID to this */
+					   relarrayname,	/* Array type name */
+					   relnamespace,	/* Same namespace as parent */
+					   InvalidOid,		/* Not composite, no relationOid */
+					   0,		/* relkind, also N/A here */
+					   ownerid, /* owner's ID */
+					   -1,		/* Internal size (varlena) */
+					   TYPTYPE_BASE,	/* Not composite - typelem is */
+					   TYPCATEGORY_ARRAY,		/* type-category (array) */
+					   false,	/* array types are never preferred */
+					   DEFAULT_TYPDELIM,		/* default array delimiter */
+					   F_ARRAY_IN,		/* array input proc */
+					   F_ARRAY_OUT,		/* array output proc */
+					   F_ARRAY_RECV,	/* array recv (bin) proc */
+					   F_ARRAY_SEND,	/* array send (bin) proc */
+					   InvalidOid,		/* typmodin procedure - none */
+					   InvalidOid,		/* typmodout procedure - none */
+					   F_ARRAY_TYPANALYZE,		/* array analyze procedure */
+					   new_type_oid,	/* array element type - the rowtype */
+					   true,	/* yes, this is an array type */
+					   InvalidOid,		/* this has no array type */
+					   InvalidOid,		/* domain base type - irrelevant */
+					   NULL,	/* default value - none */
+					   NULL,	/* default binary representation */
+					   false,	/* passed by reference */
+					   'd',		/* alignment - must be the largest! */
+					   'x',		/* fully TOASTable */
+					   -1,		/* typmod */
+					   0,		/* array dimensions for typBaseType */
+					   false,	/* Type NOT NULL */
+					   InvalidOid);		/* rowtypes never have a collation */
+
+			/* Clear relpersistence hint. */
+			fasttab_clear_relpersistence_hint();
+
+			pfree(relarrayname);
+		}
+
+	}
+	PG_CATCH();
+	{
+		/* clear relpersistence hint in case of error */
+		fasttab_clear_relpersistence_hint();
+		PG_RE_THROW();
 	}
+	PG_END_TRY();
+
 
 	/*
 	 * now create an entry in pg_class for the relation.
@@ -1308,7 +1334,7 @@ heap_create_with_catalog(const char *relname,
 
 		recordDependencyOnOwner(RelationRelationId, relid, ownerid);
 
-		if (relpersistence != RELPERSISTENCE_TEMP)
+		if (relpersistence != RELPERSISTENCE_TEMP && relpersistence != RELPERSISTENCE_FAST_TEMP)
 			recordDependencyOnCurrentExtension(&myself, false);
 
 		if (reloftypeid)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 7b30e46..aa448f8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -30,6 +30,7 @@
 #include "access/transam.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
+#include "access/fasttab.h"
 #include "bootstrap/bootstrap.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
@@ -2284,6 +2285,14 @@ IndexBuildHeapRangeScan(Relation heapRelation,
 	{
 		bool		tupleIsAlive;
 
+		/*
+		 * Ignore in-memory tuples here. These tuples are in fact not indexed.
+		 * They are mixed in during index scans right from the virtual heap
+		 * instead.
+		 */
+		if (IsFasttabItemPointer(&heapTuple->t_self))
+			continue;
+
 		CHECK_FOR_INTERRUPTS();
 
 		/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 8fd4c31..47dc2aa 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -284,7 +284,7 @@ RangeVarGetRelidExtended(const RangeVar *relation, LOCKMODE lockmode,
 		 * operation, which must be careful to find the temp table, even when
 		 * pg_temp is not first in the search path.
 		 */
-		if (relation->relpersistence == RELPERSISTENCE_TEMP)
+		if (relation->relpersistence == RELPERSISTENCE_TEMP || relation->relpersistence == RELPERSISTENCE_FAST_TEMP)
 		{
 			if (!OidIsValid(myTempNamespace))
 				relId = InvalidOid;		/* this probably can't happen? */
@@ -463,7 +463,7 @@ RangeVarGetCreationNamespace(const RangeVar *newRelation)
 		namespaceId = get_namespace_oid(newRelation->schemaname, false);
 		/* we do not check for USAGE rights here! */
 	}
-	else if (newRelation->relpersistence == RELPERSISTENCE_TEMP)
+	else if (newRelation->relpersistence == RELPERSISTENCE_TEMP || newRelation->relpersistence == RELPERSISTENCE_FAST_TEMP)
 	{
 		/* Initialize temp namespace if first time through */
 		if (!OidIsValid(myTempNamespace))
@@ -631,6 +631,7 @@ RangeVarAdjustRelationPersistence(RangeVar *newRelation, Oid nspid)
 	switch (newRelation->relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			if (!isTempOrTempToastNamespace(nspid))
 			{
 				if (isAnyTempNamespace(nspid))
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index 0d8311c..99bcf5b 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -85,6 +85,7 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
 	switch (relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			backend = BackendIdForTempRelations();
 			needs_wal = false;
 			break;
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 43bbd90..2036ece 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -25,6 +25,7 @@
 #include "access/tuptoaster.h"
 #include "access/xact.h"
 #include "access/xlog.h"
+#include "access/fasttab.h"
 #include "catalog/pg_am.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
@@ -641,7 +642,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, char relpersistence,
 	if (isNull)
 		reloptions = (Datum) 0;
 
-	if (relpersistence == RELPERSISTENCE_TEMP)
+	if (relpersistence == RELPERSISTENCE_TEMP || relpersistence == RELPERSISTENCE_FAST_TEMP)
 		namespaceid = LookupCreationNamespace("pg_temp");
 	else
 		namespaceid = RelationGetNamespace(OldHeap);
@@ -952,6 +953,10 @@ copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 			if (tuple == NULL)
 				break;
 
+			/* No need to move in-memory tuple anywhere */
+			if (IsFasttabItemPointer(&tuple->t_self))
+				continue;
+
 			/* Since we used no scan keys, should never need to recheck */
 			if (indexScan->xs_recheck)
 				elog(ERROR, "CLUSTER does not support lossy index conditions");
@@ -964,6 +969,10 @@ copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 			if (tuple == NULL)
 				break;
 
+			/* No need to move in-memory tuple anywhere */
+			if (IsFasttabItemPointer(&tuple->t_self))
+				continue;
+
 			buf = heapScan->rs_cbuf;
 		}
 
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index d14d540..49a0ab2 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1945,7 +1945,7 @@ ReindexMultipleTables(const char *objectName, ReindexObjectType objectKind,
 			continue;
 
 		/* Skip temp tables of other backends; we can't reindex them at all */
-		if (classtuple->relpersistence == RELPERSISTENCE_TEMP &&
+		if ((classtuple->relpersistence == RELPERSISTENCE_TEMP || classtuple->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
 			!isTempNamespace(classtuple->relnamespace))
 			continue;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 86e9814..f5cd689 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -22,6 +22,7 @@
 #include "access/sysattr.h"
 #include "access/xact.h"
 #include "access/xlog.h"
+#include "access/fasttab.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
 #include "catalog/heap.h"
@@ -487,7 +488,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	 * Check consistency of arguments
 	 */
 	if (stmt->oncommit != ONCOMMIT_NOOP
-		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP
+		&& stmt->relation->relpersistence != RELPERSISTENCE_FAST_TEMP)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 				 errmsg("ON COMMIT can only be used on temporary tables")));
@@ -506,7 +508,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	 * code.  This is needed because calling code might not expect untrusted
 	 * tables to appear in pg_temp at the front of its search path.
 	 */
-	if (stmt->relation->relpersistence == RELPERSISTENCE_TEMP
+	if ((stmt->relation->relpersistence == RELPERSISTENCE_TEMP ||
+		 stmt->relation->relpersistence == RELPERSISTENCE_FAST_TEMP)
 		&& InSecurityRestrictedOperation())
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1529,14 +1532,17 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 							parent->relname)));
 		/* Permanent rels cannot inherit from temporary ones */
 		if (relpersistence != RELPERSISTENCE_TEMP &&
-			relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+			relpersistence != RELPERSISTENCE_FAST_TEMP &&
+			(relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+			 relation->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP))
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot inherit from temporary relation \"%s\"",
 							parent->relname)));
 
 		/* If existing rel is temp, it must belong to this session */
-		if (relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+		if ((relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+			 relation->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
 			!relation->rd_islocaltemp)
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -6303,7 +6309,9 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
 						 errmsg("constraints on unlogged tables may reference only permanent or unlogged tables")));
 			break;
 		case RELPERSISTENCE_TEMP:
-			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
+		case RELPERSISTENCE_FAST_TEMP:
+			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+				pkrel->rd_rel->relpersistence != RELPERSISTENCE_FAST_TEMP)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 						 errmsg("constraints on temporary tables may reference only temporary tables")));
@@ -10046,22 +10054,26 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode)
 	ATSimplePermissions(parent_rel, ATT_TABLE | ATT_FOREIGN_TABLE);
 
 	/* Permanent rels cannot inherit from temporary ones */
-	if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
-		child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
+	if ((parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		 parent_rel->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
+		(child_rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP &&
+		 child_rel->rd_rel->relpersistence != RELPERSISTENCE_FAST_TEMP))
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("cannot inherit from temporary relation \"%s\"",
 						RelationGetRelationName(parent_rel))));
 
 	/* If parent rel is temp, it must belong to this session */
-	if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+	if ((parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		 parent_rel->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
 		!parent_rel->rd_islocaltemp)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 		errmsg("cannot inherit from temporary relation of another session")));
 
 	/* Ditto for the child */
-	if (child_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
+	if ((child_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP ||
+		 child_rel->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP) &&
 		!child_rel->rd_islocaltemp)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -11264,6 +11276,7 @@ ATPrepChangePersistence(Relation rel, bool toLogged)
 	switch (rel->rd_rel->relpersistence)
 	{
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 					 errmsg("cannot change logged status of table \"%s\" because it is temporary",
diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c
index 7902d43..28c4603 100644
--- a/src/backend/commands/tablespace.c
+++ b/src/backend/commands/tablespace.c
@@ -1117,7 +1117,7 @@ GetDefaultTablespace(char relpersistence)
 	Oid			result;
 
 	/* The temp-table case is handled elsewhere */
-	if (relpersistence == RELPERSISTENCE_TEMP)
+	if (relpersistence == RELPERSISTENCE_TEMP || relpersistence == RELPERSISTENCE_FAST_TEMP)
 	{
 		PrepareTempTablespaces();
 		return GetNextTempTableSpace();
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index 449aacb..3845a25 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -37,6 +37,7 @@
 
 #include "access/relscan.h"
 #include "access/transam.h"
+#include "access/fasttab.h"
 #include "executor/execdebug.h"
 #include "executor/nodeBitmapHeapscan.h"
 #include "pgstat.h"
@@ -153,28 +154,40 @@ BitmapHeapNext(BitmapHeapScanState *node)
 			}
 #endif   /* USE_PREFETCH */
 
-			/*
-			 * Ignore any claimed entries past what we think is the end of the
-			 * relation.  (This is probably not necessary given that we got at
-			 * least AccessShareLock on the table before performing any of the
-			 * indexscans, but let's be safe.)
-			 */
-			if (tbmres->blockno >= scan->rs_nblocks)
-			{
-				node->tbmres = tbmres = NULL;
-				continue;
-			}
-
-			/*
-			 * Fetch the current heap page and identify candidate tuples.
-			 */
-			bitgetpage(scan, tbmres);
-
 			if (tbmres->ntuples >= 0)
 				node->exact_pages++;
 			else
 				node->lossy_pages++;
 
+			if(tbmres->blockno < scan->rs_nblocks)
+			{
+				/*
+				 * Normal case. Fetch the current heap page and identify
+				 * candidate tuples.
+				 */
+				bitgetpage(scan, tbmres);
+			}
+			else
+			{
+				/*
+				 * Probably we are looking for in-memory tuple. This code
+				 * executes in cases when CurrentFasttabBlockId is larger than
+				 * normal block id's.
+				 */
+				OffsetNumber i;
+
+				/*
+				 * Check all tuples on a virtual page.
+				 *
+				 * NB: 0 is an invalid offset.
+				 */
+				for(i = 1; i <= MaxHeapTuplesPerPage; i++)
+					scan->rs_vistuples[i-1] = FASTTAB_ITEM_POINTER_BIT | i;
+
+				scan->rs_ntuples = MaxHeapTuplesPerPage;
+				tbmres->recheck = true;
+			}
+
 			/*
 			 * Set rs_cindex to first slot to examine
 			 */
@@ -257,15 +270,27 @@ BitmapHeapNext(BitmapHeapScanState *node)
 		 * Okay to fetch the tuple
 		 */
 		targoffset = scan->rs_vistuples[scan->rs_cindex];
-		dp = (Page) BufferGetPage(scan->rs_cbuf);
-		lp = PageGetItemId(dp, targoffset);
-		Assert(ItemIdIsNormal(lp));
-
-		scan->rs_ctup.t_data = (HeapTupleHeader) PageGetItem((Page) dp, lp);
-		scan->rs_ctup.t_len = ItemIdGetLength(lp);
-		scan->rs_ctup.t_tableOid = scan->rs_rd->rd_id;
 		ItemPointerSet(&scan->rs_ctup.t_self, tbmres->blockno, targoffset);
 
+		/* Is it a virtual TID? */
+		if (IsFasttabItemPointer(&scan->rs_ctup.t_self))
+		{
+			/* Fetch tuple from virtual catalog (if tuple still exists). */
+			if(!fasttab_simple_heap_fetch(scan->rs_rd, scan->rs_snapshot, &scan->rs_ctup))
+				continue;
+		}
+		else
+		{
+			/* Regular logic. */
+			dp = (Page) BufferGetPage(scan->rs_cbuf);
+			lp = PageGetItemId(dp, targoffset);
+			Assert(ItemIdIsNormal(lp));
+
+			scan->rs_ctup.t_data = (HeapTupleHeader) PageGetItem((Page) dp, lp);
+			scan->rs_ctup.t_len = ItemIdGetLength(lp);
+			scan->rs_ctup.t_tableOid = scan->rs_rd->rd_id;
+		}
+
 		pgstat_count_heap_fetch(scan->rs_rd);
 
 		/*
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index dfeb7d5..8ede443 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -41,17 +41,19 @@
 #include <limits.h>
 
 #include "access/htup_details.h"
+#include "access/fasttab.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
 #include "utils/hsearch.h"
 
 /*
  * The maximum number of tuples per page is not large (typically 256 with
- * 8K pages, or 1024 with 32K pages).  So there's not much point in making
- * the per-page bitmaps variable size.  We just legislate that the size
+ * 8K pages, or 1024 with 32K pages).  Also in-memory tuples have large fake
+ * offsets because of FASTTAB_ITEM_POINTER_BIT. So there's not much point in
+ * making the per-page bitmaps variable size.  We just legislate that the size
  * is this:
  */
-#define MAX_TUPLES_PER_PAGE  MaxHeapTuplesPerPage
+#define MAX_TUPLES_PER_PAGE (FASTTAB_ITEM_POINTER_BIT | MaxHeapTuplesPerPage)
 
 /*
  * When we have to switch over to lossy storage, we use a data structure
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 88d833a..976ca06 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -501,6 +501,8 @@ static void
 set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 						  RangeTblEntry *rte)
 {
+	char relpersistence;
+
 	/*
 	 * The flag has previously been initialized to false, so we can just
 	 * return if it becomes clear that we can't safely set it.
@@ -529,7 +531,8 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 			 * the rest of the necessary infrastructure right now anyway.  So
 			 * for now, bail out if we see a temporary table.
 			 */
-			if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP)
+			relpersistence = get_rel_persistence(rte->relid);
+			if (relpersistence == RELPERSISTENCE_TEMP || relpersistence == RELPERSISTENCE_FAST_TEMP)
 				return;
 
 			/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0cae446..e5220d2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -586,7 +586,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
 	EXTENSION EXTERNAL EXTRACT
 
-	FALSE_P FAMILY FETCH FILTER FIRST_P FLOAT_P FOLLOWING FOR
+	FALSE_P FAMILY FAST FETCH FILTER FIRST_P FLOAT_P FOLLOWING FOR
 	FORCE FOREIGN FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
 
 	GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING
@@ -2891,6 +2891,8 @@ OptTemp:	TEMPORARY					{ $$ = RELPERSISTENCE_TEMP; }
 			| TEMP						{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMPORARY			{ $$ = RELPERSISTENCE_TEMP; }
 			| LOCAL TEMP				{ $$ = RELPERSISTENCE_TEMP; }
+			| FAST TEMPORARY			{ $$ = RELPERSISTENCE_FAST_TEMP; }
+			| FAST TEMP				{ $$ = RELPERSISTENCE_FAST_TEMP; }
 			| GLOBAL TEMPORARY
 				{
 					ereport(WARNING,
@@ -10280,6 +10282,16 @@ OptTempTableName:
 					$$ = $4;
 					$$->relpersistence = RELPERSISTENCE_TEMP;
 				}
+			| FAST TEMPORARY opt_table qualified_name
+				{
+					$$ = $4;
+					$$->relpersistence = RELPERSISTENCE_FAST_TEMP;
+				}
+			| FAST TEMP opt_table qualified_name
+				{
+					$$ = $4;
+					$$->relpersistence = RELPERSISTENCE_FAST_TEMP;
+				}
 			| GLOBAL TEMPORARY opt_table qualified_name
 				{
 					ereport(WARNING,
@@ -13807,6 +13819,7 @@ unreserved_keyword:
 			| EXTENSION
 			| EXTERNAL
 			| FAMILY
+			| FAST
 			| FILTER
 			| FIRST_P
 			| FOLLOWING
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 1e3ecbc..1ed569d 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -3141,7 +3141,7 @@ isQueryUsingTempRelation_walker(Node *node, void *context)
 				char		relpersistence = rel->rd_rel->relpersistence;
 
 				heap_close(rel, AccessShareLock);
-				if (relpersistence == RELPERSISTENCE_TEMP)
+				if (relpersistence == RELPERSISTENCE_TEMP || relpersistence == RELPERSISTENCE_FAST_TEMP)
 					return true;
 			}
 		}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e98fad0..7f71706 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -202,7 +202,8 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 	 * specified to be in pg_temp, so no need for anything extra in that case.
 	 */
 	if (stmt->relation->schemaname == NULL
-		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP)
+		&& stmt->relation->relpersistence != RELPERSISTENCE_TEMP
+		&& stmt->relation->relpersistence != RELPERSISTENCE_FAST_TEMP)
 		stmt->relation->schemaname = get_namespace_name(namespaceid);
 
 	/* Set up CreateStmtContext */
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 3768f50..4fe00ee 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2037,7 +2037,8 @@ do_autovacuum(void)
 		 * Check if it is a temp table (presumably, of some other backend's).
 		 * We cannot safely process other backends' temp tables.
 		 */
-		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+		if (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+			classForm->relpersistence == RELPERSISTENCE_FAST_TEMP)
 		{
 			int			backendID;
 
@@ -2134,7 +2135,8 @@ do_autovacuum(void)
 		/*
 		 * We cannot safely process other backends' temp tables, so skip 'em.
 		 */
-		if (classForm->relpersistence == RELPERSISTENCE_TEMP)
+		if (classForm->relpersistence == RELPERSISTENCE_TEMP ||
+			classForm->relpersistence == RELPERSISTENCE_FAST_TEMP)
 			continue;
 
 		relid = HeapTupleGetOid(tuple);
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 0776f3b..a0ebbad 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -1003,6 +1003,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS)
 			backend = InvalidBackendId;
 			break;
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			if (isTempOrTempToastNamespace(relform->relnamespace))
 				backend = BackendIdForTempRelations();
 			else
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8d2ad01..f811afe 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -991,6 +991,7 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 			relation->rd_islocaltemp = false;
 			break;
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			if (isTempOrTempToastNamespace(relation->rd_rel->relnamespace))
 			{
 				relation->rd_backend = BackendIdForTempRelations();
@@ -1937,6 +1938,7 @@ RelationReloadIndexInfo(Relation relation)
 			 RelationGetRelid(relation));
 	relp = (Form_pg_class) GETSTRUCT(pg_class_tuple);
 	memcpy(relation->rd_rel, relp, CLASS_TUPLE_SIZE);
+
 	/* Reload reloptions in case they changed */
 	if (relation->rd_options)
 		pfree(relation->rd_options);
@@ -2974,6 +2976,7 @@ RelationBuildLocalRelation(const char *relname,
 			rel->rd_islocaltemp = false;
 			break;
 		case RELPERSISTENCE_TEMP:
+		case RELPERSISTENCE_FAST_TEMP:
 			Assert(isTempOrTempToastNamespace(relnamespace));
 			rel->rd_backend = BackendIdForTempRelations();
 			rel->rd_islocaltemp = true;
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8469d9f..d3ccac4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -894,6 +894,7 @@ static const pgsql_thing_t words_after_create[] = {
 	{"DOMAIN", NULL, &Query_for_list_of_domains},
 	{"EVENT TRIGGER", NULL, NULL},
 	{"EXTENSION", Query_for_list_of_extensions},
+	{"FAST TEMP", NULL, NULL, THING_NO_DROP},		/* for CREATE FAST TEMP TABLE ... */
 	{"FOREIGN DATA WRAPPER", NULL, NULL},
 	{"FOREIGN TABLE", NULL, NULL},
 	{"FUNCTION", NULL, &Query_for_list_of_functions},
diff --git a/src/include/access/fasttab.h b/src/include/access/fasttab.h
new file mode 100644
index 0000000..4be68ca
--- /dev/null
+++ b/src/include/access/fasttab.h
@@ -0,0 +1,102 @@
+/*-------------------------------------------------------------------------
+ *
+ * fasttab.h
+ *	  virtual catalog and fast temporary tables
+ *
+ * FOR INTERNAL USAGE ONLY. Backward compatability is not guaranteed.
+ * Don't use in extensions!
+ *
+ * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/access/fasttab.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FASTTAB_H
+#define FASTTAB_H
+
+#include "c.h"
+#include "postgres_ext.h"
+#include "access/htup.h"
+#include "access/heapam.h"
+#include "access/sdir.h"
+#include "access/genam.h"
+#include "catalog/indexing.h"
+#include "storage/itemptr.h"
+#include "utils/relcache.h"
+
+/*
+ * Flag stored in ItemPointerData.ip_posid to mark tuple as virtual. We can
+ * safely store a flag in higher bits of ip_posid since it's maximum value is
+ * very limited. See MaxHeapTuplesPerPage.
+ *
+ * This constant better be not too large since MAX_TUPLES_PER_PAGE depends on
+ * its value.
+ */
+#define FASTTAB_ITEM_POINTER_BIT 0x0800
+
+/* Determine whether ItemPointer is virtual */
+#define IsFasttabItemPointer(ptr) \
+	( ((ptr)->ip_posid & FASTTAB_ITEM_POINTER_BIT) != 0 )
+
+typedef struct FasttabIndexMethodsData FasttabIndexMethodsData;
+
+typedef FasttabIndexMethodsData const *FasttabIndexMethods;
+
+extern bool IsFasttabHandledRelationId(Oid relId);
+
+extern bool IsFasttabHandledIndexId(Oid indexId);
+
+extern void fasttab_set_relpersistence_hint(char relpersistence);
+
+extern void fasttab_clear_relpersistence_hint(void);
+
+extern void fasttab_begin_transaction(void);
+
+extern void fasttab_end_transaction(void);
+
+extern void fasttab_abort_transaction(void);
+
+extern void fasttab_define_savepoint(const char *name);
+
+extern void fasttab_rollback_to_savepoint(const char *name);
+
+extern void fasttab_beginscan(HeapScanDesc scan);
+
+extern HeapTuple fasttab_getnext(HeapScanDesc scan, ScanDirection direction);
+
+extern bool fasttab_hot_search_buffer(ItemPointer tid, Relation relation,
+						  HeapTuple heapTuple, bool *all_dead, bool *result);
+
+extern bool fasttab_insert(Relation relation, HeapTuple tup, HeapTuple heaptup,
+			   Oid *result);
+
+extern bool fasttab_delete(Relation relation, ItemPointer tid);
+
+extern bool fasttab_update(Relation relation, ItemPointer otid,
+			   HeapTuple newtup);
+
+extern bool fasttab_inplace_update(Relation relation, HeapTuple tuple);
+
+extern bool fasttab_index_insert(Relation indexRelation,
+					 ItemPointer heap_t_ctid, bool *result);
+
+extern void fasttab_index_beginscan(IndexScanDesc scan);
+
+extern void fasttab_index_rescan(IndexScanDesc scan, ScanKey keys, int nkeys,
+					 ScanKey orderbys, int norderbys);
+
+extern bool fasttab_simple_heap_fetch(Relation relation, Snapshot snapshot,
+						  HeapTuple tuple);
+
+extern bool fasttab_index_getnext_tid_merge(IndexScanDesc scan,
+								ScanDirection direction);
+
+extern bool fasttab_index_getbitmap(IndexScanDesc scan, TIDBitmap *bitmap,
+						int64 *result);
+
+extern void fasttab_index_endscan(IndexScanDesc scan);
+
+#endif   /* FASTTAB_H */
diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 49c2a6f..bf61f60 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -19,6 +19,7 @@
 #include "access/htup_details.h"
 #include "access/itup.h"
 #include "access/tupdesc.h"
+#include "access/fasttab.h"
 
 /*
  * Shared state for parallel heap scan.
@@ -74,6 +75,8 @@ typedef struct HeapScanDescData
 	int			rs_cindex;		/* current tuple's index in vistuples */
 	int			rs_ntuples;		/* number of visible tuples on page */
 	OffsetNumber rs_vistuples[MaxHeapTuplesPerPage];	/* their offsets */
+
+	dlist_node *rs_curr_inmem_tupnode;	/* current virtual tuple, or NULL */
 }	HeapScanDescData;
 
 /*
@@ -125,6 +128,19 @@ typedef struct IndexScanDescData
 
 	/* state data for traversing HOT chains in index_getnext */
 	bool		xs_continue_hot;	/* T if must keep walking HOT chain */
+
+	/* sorted list of virtual tuples that should be returned during a scan */
+	dlist_head	xs_inmem_tuplist;
+	/* memoized corresponding FasttabIndexMethodsTable entry */
+	FasttabIndexMethods indexMethods;
+	/* whether xs_inmem_tuplist was initialized */
+	bool		xs_inmem_tuplist_init_done;
+	/* whether xs_inmem_tuplist contains a regular tuple */
+	bool		xs_regular_tuple_enqueued;
+	/* whether internal regular scan was finished */
+	bool		xs_regular_scan_finished;
+	/* whether information that scan was finished was returned */
+	bool		xs_scan_finish_returned;
 }	IndexScanDescData;
 
 /* Struct for heap-or-index scans of system tables */
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index e57b81c..f51a91a 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -162,9 +162,11 @@ DESCR("");
 #define		  RELKIND_FOREIGN_TABLE   'f'		/* foreign table */
 #define		  RELKIND_MATVIEW		  'm'		/* materialized view */
 
+#define		  RELPERSISTENCE_UNDEFINED  '?'		/* invalid relpersistence value */
 #define		  RELPERSISTENCE_PERMANENT	'p'		/* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u'		/* unlogged permanent table */
 #define		  RELPERSISTENCE_TEMP		't'		/* temporary table */
+#define		  RELPERSISTENCE_FAST_TEMP	'f'		/* fast temporary table */
 
 /* default selection for replica identity (primary key or nothing) */
 #define		  REPLICA_IDENTITY_DEFAULT	'd'
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 17ffef5..a673176 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -157,6 +157,7 @@ PG_KEYWORD("external", EXTERNAL, UNRESERVED_KEYWORD)
 PG_KEYWORD("extract", EXTRACT, COL_NAME_KEYWORD)
 PG_KEYWORD("false", FALSE_P, RESERVED_KEYWORD)
 PG_KEYWORD("family", FAMILY, UNRESERVED_KEYWORD)
+PG_KEYWORD("fast", FAST, UNRESERVED_KEYWORD)
 PG_KEYWORD("fetch", FETCH, RESERVED_KEYWORD)
 PG_KEYWORD("filter", FILTER, UNRESERVED_KEYWORD)
 PG_KEYWORD("first", FIRST_P, UNRESERVED_KEYWORD)
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index ed14442..543fad0 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -463,9 +463,11 @@ typedef struct ViewOptions
 /*
  * RelationUsesLocalBuffers
  *		True if relation's pages are stored in local buffers.
+ *
+ * Beware of multiple eval of argument
  */
 #define RelationUsesLocalBuffers(relation) \
-	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+	(((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP) || ((relation)->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP))
 
 /*
  * RELATION_IS_LOCAL
@@ -486,7 +488,7 @@ typedef struct ViewOptions
  * Beware of multiple eval of argument
  */
 #define RELATION_IS_OTHER_TEMP(relation) \
-	((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP && \
+	((((relation)->rd_rel->relpersistence == RELPERSISTENCE_TEMP) || ((relation)->rd_rel->relpersistence == RELPERSISTENCE_FAST_TEMP)) && \
 	 !(relation)->rd_islocaltemp)
 
 
diff --git a/src/test/regress/expected/fast_temp.out b/src/test/regress/expected/fast_temp.out
new file mode 100644
index 0000000..91945d3
--- /dev/null
+++ b/src/test/regress/expected/fast_temp.out
@@ -0,0 +1,406 @@
+--
+-- FAST TEMP
+-- Test fast temporary tables
+--
+-- basic test
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+INSERT INTO fasttab_test1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'), (4, 'ddd');
+UPDATE fasttab_test1 SET s = 'eee' WHERE x = 4;
+UPDATE fasttab_test1 SET x = 5 WHERE s = 'bbb';
+DELETE FROM fasttab_test1 WHERE x = 3;
+SELECT * FROM fasttab_test1 ORDER BY x;
+ x |  s  
+---+-----
+ 1 | aaa
+ 4 | eee
+ 5 | bbb
+(3 rows)
+
+DROP TABLE fasttab_test1;
+-- kind of load test
+do $$
+declare
+  count_fast_table integer = 150;
+  count_attr integer = 20;
+  i integer;
+  j integer;
+  t_sql text;
+begin
+  for i in 1 .. count_fast_table
+  loop
+    t_sql = 'CREATE FAST TEMP TABLE fast_table_' || i :: text;
+    t_sql = t_sql || '  (';
+    for j in 1 .. count_attr
+    loop
+      t_sql = t_sql || ' attr' || j || ' text';
+      if j <> count_attr then
+        t_sql = t_sql || ', ';
+      end if;
+    end loop;
+    t_sql = t_sql || ' );';
+    execute t_sql;
+    -- raise info 't_sql %', t_sql;
+  end loop;
+end $$;
+SELECT * FROM fast_table_1;
+ attr1 | attr2 | attr3 | attr4 | attr5 | attr6 | attr7 | attr8 | attr9 | attr10 | attr11 | attr12 | attr13 | attr14 | attr15 | attr16 | attr17 | attr18 | attr19 | attr20 
+-------+-------+-------+-------+-------+-------+-------+-------+-------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------
+(0 rows)
+
+-- test bitmap index scan
+SELECT count(*) FROM pg_class WHERE relname = 'fast_table_1' OR relname = 'fast_table_2';
+ count 
+-------
+     2
+(1 row)
+
+-- create / delete / create test
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+-- check index only scan
+SELECT COUNT(*) FROM pg_class WHERE relname = 'fasttab_test1';
+ count 
+-------
+     1
+(1 row)
+
+SELECT relname FROM pg_class WHERE relname = 'fasttab_test1';
+    relname    
+---------------
+ fasttab_test1
+(1 row)
+
+DROP TABLE fasttab_test1;
+-- select from non-existend temp table
+SELECT COUNT(*) FROM fasttab_test1;
+ERROR:  relation "fasttab_test1" does not exist
+LINE 1: SELECT COUNT(*) FROM fasttab_test1;
+                             ^
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+CREATE FAST TEMP TABLE fasttab_test2(x int, s text);
+SELECT * FROM fasttab_test1;
+ x | s 
+---+---
+(0 rows)
+
+-- check that ALTER works as expected
+ALTER TABLE fasttab_test1 ADD COLUMN y int;
+SELECT * FROM fasttab_test1;
+ x | s | y 
+---+---+---
+(0 rows)
+
+ALTER TABLE fasttab_test1 ADD COLUMN z int;
+SELECT * FROM fasttab_test1;
+ x | s | y | z 
+---+---+---+---
+(0 rows)
+
+ALTER TABLE fasttab_test1 DROP COLUMN x;
+SELECT * FROM fasttab_test1;
+ s | y | z 
+---+---+---
+(0 rows)
+
+ALTER TABLE fasttab_test1 DROP COLUMN y;
+SELECT * FROM fasttab_test1;
+ s | z 
+---+---
+(0 rows)
+
+-- check tat ALTER TABLE ... RENAME TO ... works as expected
+CREATE FAST TEMP TABLE fast_temp_1 (x int);
+ALTER TABLE fast_temp_1 RENAME TO fast_temp_2;
+CREATE FAST TEMP TABLE fast_temp_1 (x int);
+DROP TABLE fast_temp_1;
+DROP TABLE fast_temp_2;
+-- test transactions and savepoints
+BEGIN;
+INSERT INTO fasttab_test2 VALUES (1, 'aaa'), (2, 'bbb');
+SELECT * FROM fasttab_test2;
+ x |  s  
+---+-----
+ 1 | aaa
+ 2 | bbb
+(2 rows)
+
+ROLLBACK;
+SELECT * FROM fasttab_test2;
+ x | s 
+---+---
+(0 rows)
+
+BEGIN;
+INSERT INTO fasttab_test2 VALUES (3, 'ccc'), (4, 'ddd');
+SELECT * FROM fasttab_test2;
+ x |  s  
+---+-----
+ 3 | ccc
+ 4 | ddd
+(2 rows)
+
+COMMIT;
+SELECT * FROM fasttab_test2;
+ x |  s  
+---+-----
+ 3 | ccc
+ 4 | ddd
+(2 rows)
+
+BEGIN;
+SAVEPOINT sp1;
+ALTER TABLE fasttab_test2 ADD COLUMN y int;
+SELECT * FROM fasttab_test2;
+ x |  s  | y 
+---+-----+---
+ 3 | ccc |  
+ 4 | ddd |  
+(2 rows)
+
+SAVEPOINT sp2;
+INSERT INTO fasttab_test2 VALUES (5, 'eee', 6);
+SELECT * FROM fasttab_test2;
+ x |  s  | y 
+---+-----+---
+ 3 | ccc |  
+ 4 | ddd |  
+ 5 | eee | 6
+(3 rows)
+
+ROLLBACK TO SAVEPOINT sp2;
+INSERT INTO fasttab_test2 VALUES (55, 'EEE', 66);
+SELECT * FROM fasttab_test2;
+ x  |  s  | y  
+----+-----+----
+  3 | ccc |   
+  4 | ddd |   
+ 55 | EEE | 66
+(3 rows)
+
+ROLLBACK TO SAVEPOINT sp2;
+SELECT * FROM fasttab_test2;
+ x |  s  | y 
+---+-----+---
+ 3 | ccc |  
+ 4 | ddd |  
+(2 rows)
+
+COMMIT;
+DROP TABLE fasttab_test1;
+DROP TABLE fasttab_test2;
+-- test that exceptions are handled properly
+DO $$
+DECLARE
+BEGIN
+    CREATE FAST TEMP TABLE fast_exception_test(x int, y int, z int);
+    RAISE EXCEPTION 'test error';
+END $$;
+ERROR:  test error
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at RAISE
+CREATE FAST TEMP TABLE fast_exception_test(x int, y int, z int);
+DROP TABLE fast_exception_test;
+-- test that inheritance works as expected
+-- OK:
+CREATE TABLE cities (name text, population float, altitude int);
+CREATE TABLE capitals (state char(2)) INHERITS (cities);
+DROP TABLE capitals;
+DROP TABLE cities;
+-- OK:
+CREATE TABLE cities2 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals2 (state char(2)) INHERITS (cities2);
+INSERT INTO capitals2 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals2 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals2;
+  name  | population | altitude | state 
+--------+------------+----------+-------
+ Moscow |     123.45 |      789 | RU
+ Paris  |     543.21 |      987 | FR
+(2 rows)
+
+SELECT * FROM cities2;
+  name  | population | altitude 
+--------+------------+----------
+ Moscow |     123.45 |      789
+ Paris  |     543.21 |      987
+(2 rows)
+
+DELETE FROM cities2 WHERE name = 'Moscow';
+SELECT * FROM capitals2;
+ name  | population | altitude | state 
+-------+------------+----------+-------
+ Paris |     543.21 |      987 | FR
+(1 row)
+
+SELECT * FROM cities2;
+ name  | population | altitude 
+-------+------------+----------
+ Paris |     543.21 |      987
+(1 row)
+
+DROP TABLE capitals2;
+DROP TABLE cities2;
+-- ERROR:
+CREATE FAST TEMPORARY TABLE cities3 (name text, population float, altitude int);
+-- cannot inherit from temporary relation "cities3"
+CREATE TABLE capitals3 (state char(2)) INHERITS (cities3);
+ERROR:  cannot inherit from temporary relation "cities3"
+DROP TABLE cities3;
+-- OK:
+CREATE FAST TEMPORARY TABLE cities4 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals4 (state char(2)) INHERITS (cities4);
+INSERT INTO capitals4 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals4 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals4;
+  name  | population | altitude | state 
+--------+------------+----------+-------
+ Moscow |     123.45 |      789 | RU
+ Paris  |     543.21 |      987 | FR
+(2 rows)
+
+SELECT * FROM cities4;
+  name  | population | altitude 
+--------+------------+----------
+ Moscow |     123.45 |      789
+ Paris  |     543.21 |      987
+(2 rows)
+
+DELETE FROM cities4 WHERE name = 'Moscow';
+SELECT * FROM capitals4;
+ name  | population | altitude | state 
+-------+------------+----------+-------
+ Paris |     543.21 |      987 | FR
+(1 row)
+
+SELECT * FROM cities4;
+ name  | population | altitude 
+-------+------------+----------
+ Paris |     543.21 |      987
+(1 row)
+
+DROP TABLE capitals4;
+DROP TABLE cities4;
+-- OK:
+CREATE TEMPORARY TABLE cities5 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals5 (state char(2)) INHERITS (cities5);
+INSERT INTO capitals5 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals5 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals5;
+  name  | population | altitude | state 
+--------+------------+----------+-------
+ Moscow |     123.45 |      789 | RU
+ Paris  |     543.21 |      987 | FR
+(2 rows)
+
+SELECT * FROM cities5;
+  name  | population | altitude 
+--------+------------+----------
+ Moscow |     123.45 |      789
+ Paris  |     543.21 |      987
+(2 rows)
+
+DELETE FROM cities5 WHERE name = 'Moscow';
+SELECT * FROM capitals5;
+ name  | population | altitude | state 
+-------+------------+----------+-------
+ Paris |     543.21 |      987 | FR
+(1 row)
+
+SELECT * FROM cities5;
+ name  | population | altitude 
+-------+------------+----------
+ Paris |     543.21 |      987
+(1 row)
+
+DROP TABLE capitals5;
+DROP TABLE cities5;
+-- OK:
+CREATE FAST TEMPORARY TABLE cities6 (name text, population float, altitude int);
+CREATE TEMPORARY TABLE capitals6 (state char(2)) INHERITS (cities6);
+INSERT INTO capitals6 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals6 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals6;
+  name  | population | altitude | state 
+--------+------------+----------+-------
+ Moscow |     123.45 |      789 | RU
+ Paris  |     543.21 |      987 | FR
+(2 rows)
+
+SELECT * FROM cities6;
+  name  | population | altitude 
+--------+------------+----------
+ Moscow |     123.45 |      789
+ Paris  |     543.21 |      987
+(2 rows)
+
+DELETE FROM cities6 WHERE name = 'Moscow';
+SELECT * FROM capitals6;
+ name  | population | altitude | state 
+-------+------------+----------+-------
+ Paris |     543.21 |      987 | FR
+(1 row)
+
+SELECT * FROM cities6;
+ name  | population | altitude 
+-------+------------+----------
+ Paris |     543.21 |      987
+(1 row)
+
+DROP TABLE capitals6;
+DROP TABLE cities6;
+-- test index-only scan
+CREATE FAST TEMP TABLE fasttab_unique_prefix_beta(x int);
+CREATE TABLE fasttab_unique_prefix_alpha(x int);
+CREATE FAST TEMP TABLE fasttab_unique_prefix_delta(x int);
+CREATE TABLE fasttab_unique_prefix_epsilon(x int);
+CREATE TABLE fasttab_unique_prefix_gamma(x int);
+SELECT relname FROM pg_class WHERE relname > 'fasttab_unique_prefix_' ORDER BY relname LIMIT 5;
+            relname            
+-------------------------------
+ fasttab_unique_prefix_alpha
+ fasttab_unique_prefix_beta
+ fasttab_unique_prefix_delta
+ fasttab_unique_prefix_epsilon
+ fasttab_unique_prefix_gamma
+(5 rows)
+
+DROP TABLE fasttab_unique_prefix_alpha;
+DROP TABLE fasttab_unique_prefix_beta;
+DROP TABLE fasttab_unique_prefix_gamma;
+DROP TABLE fasttab_unique_prefix_delta;
+DROP TABLE fasttab_unique_prefix_epsilon;
+-- test VACUUM / VACUUM FULL
+VACUUM;
+VACUUM FULL;
+SELECT * FROM fast_table_1;
+ attr1 | attr2 | attr3 | attr4 | attr5 | attr6 | attr7 | attr8 | attr9 | attr10 | attr11 | attr12 | attr13 | attr14 | attr15 | attr16 | attr17 | attr18 | attr19 | attr20 
+-------+-------+-------+-------+-------+-------+-------+-------+-------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------
+(0 rows)
+
+-- test ANALYZE
+CREATE FAST TEMP TABLE fasttab_analyze_test(x int, s text);
+INSERT INTO fasttab_analyze_test SELECT x, '--> ' || x FROM generate_series(1,100) as x;
+ANALYZE fasttab_analyze_test;
+SELECT count(*) FROM pg_statistic WHERE starelid = (SELECT oid FROM pg_class WHERE relname = 'fasttab_analyze_test');
+ count 
+-------
+     2
+(1 row)
+
+DROP TABLE fasttab_analyze_test;
+SELECT count(*) FROM pg_statistic WHERE starelid = (SELECT oid FROM pg_class WHERE relname = 'fasttab_analyze_test');
+ count 
+-------
+     0
+(1 row)
+
+-- cleanup after load test
+do $$
+declare
+  count_fast_table integer = 150;
+  t_sql text;
+begin
+  for i in 1 .. count_fast_table
+  loop
+    t_sql = 'DROP TABLE fast_table_' || i || ';';
+    execute t_sql;
+  end loop;
+end $$;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4ebad04..60b9bec 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -105,6 +105,11 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
 
+# ----------
+# Another group of parallel tests
+# ----------
+test: fast_temp
+
 # event triggers cannot run concurrently with any test that runs DDL
 test: event_trigger
 
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 5c7038d..5a5c401 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -167,3 +167,4 @@ test: with
 test: xml
 test: event_trigger
 test: stats
+test: fast_temp
diff --git a/src/test/regress/sql/fast_temp.sql b/src/test/regress/sql/fast_temp.sql
new file mode 100644
index 0000000..e4fef5e
--- /dev/null
+++ b/src/test/regress/sql/fast_temp.sql
@@ -0,0 +1,265 @@
+--
+-- FAST TEMP
+-- Test fast temporary tables
+--
+
+-- basic test
+
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+
+INSERT INTO fasttab_test1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc'), (4, 'ddd');
+
+UPDATE fasttab_test1 SET s = 'eee' WHERE x = 4;
+
+UPDATE fasttab_test1 SET x = 5 WHERE s = 'bbb';
+
+DELETE FROM fasttab_test1 WHERE x = 3;
+
+SELECT * FROM fasttab_test1 ORDER BY x;
+
+DROP TABLE fasttab_test1;
+
+-- kind of load test
+
+do $$
+declare
+  count_fast_table integer = 150;
+  count_attr integer = 20;
+  i integer;
+  j integer;
+  t_sql text;
+begin
+  for i in 1 .. count_fast_table
+  loop
+    t_sql = 'CREATE FAST TEMP TABLE fast_table_' || i :: text;
+    t_sql = t_sql || '  (';
+    for j in 1 .. count_attr
+    loop
+      t_sql = t_sql || ' attr' || j || ' text';
+      if j <> count_attr then
+        t_sql = t_sql || ', ';
+      end if;
+    end loop;
+    t_sql = t_sql || ' );';
+    execute t_sql;
+    -- raise info 't_sql %', t_sql;
+  end loop;
+end $$;
+
+SELECT * FROM fast_table_1;
+
+-- test bitmap index scan
+
+SELECT count(*) FROM pg_class WHERE relname = 'fast_table_1' OR relname = 'fast_table_2';
+
+-- create / delete / create test
+
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+
+-- check index only scan
+
+SELECT COUNT(*) FROM pg_class WHERE relname = 'fasttab_test1';
+SELECT relname FROM pg_class WHERE relname = 'fasttab_test1';
+
+DROP TABLE fasttab_test1;
+
+-- select from non-existend temp table
+
+SELECT COUNT(*) FROM fasttab_test1;
+
+CREATE FAST TEMP TABLE fasttab_test1(x int, s text);
+CREATE FAST TEMP TABLE fasttab_test2(x int, s text);
+SELECT * FROM fasttab_test1;
+
+-- check that ALTER works as expected
+
+ALTER TABLE fasttab_test1 ADD COLUMN y int;
+SELECT * FROM fasttab_test1;
+
+ALTER TABLE fasttab_test1 ADD COLUMN z int;
+SELECT * FROM fasttab_test1;
+
+ALTER TABLE fasttab_test1 DROP COLUMN x;
+SELECT * FROM fasttab_test1;
+
+ALTER TABLE fasttab_test1 DROP COLUMN y;
+SELECT * FROM fasttab_test1;
+
+-- check tat ALTER TABLE ... RENAME TO ... works as expected
+
+CREATE FAST TEMP TABLE fast_temp_1 (x int);
+ALTER TABLE fast_temp_1 RENAME TO fast_temp_2;
+CREATE FAST TEMP TABLE fast_temp_1 (x int);
+DROP TABLE fast_temp_1;
+DROP TABLE fast_temp_2;
+
+-- test transactions and savepoints
+
+BEGIN;
+
+INSERT INTO fasttab_test2 VALUES (1, 'aaa'), (2, 'bbb');
+SELECT * FROM fasttab_test2;
+
+ROLLBACK;
+
+SELECT * FROM fasttab_test2;
+
+BEGIN;
+
+INSERT INTO fasttab_test2 VALUES (3, 'ccc'), (4, 'ddd');
+SELECT * FROM fasttab_test2;
+
+COMMIT;
+
+SELECT * FROM fasttab_test2;
+
+
+BEGIN;
+
+SAVEPOINT sp1;
+
+ALTER TABLE fasttab_test2 ADD COLUMN y int;
+SELECT * FROM fasttab_test2;
+
+SAVEPOINT sp2;
+
+INSERT INTO fasttab_test2 VALUES (5, 'eee', 6);
+SELECT * FROM fasttab_test2;
+ROLLBACK TO SAVEPOINT sp2;
+
+INSERT INTO fasttab_test2 VALUES (55, 'EEE', 66);
+SELECT * FROM fasttab_test2;
+ROLLBACK TO SAVEPOINT sp2;
+
+SELECT * FROM fasttab_test2;
+COMMIT;
+
+DROP TABLE fasttab_test1;
+DROP TABLE fasttab_test2;
+
+-- test that exceptions are handled properly
+
+DO $$
+DECLARE
+BEGIN
+    CREATE FAST TEMP TABLE fast_exception_test(x int, y int, z int);
+    RAISE EXCEPTION 'test error';
+END $$;
+
+CREATE FAST TEMP TABLE fast_exception_test(x int, y int, z int);
+DROP TABLE fast_exception_test;
+
+-- test that inheritance works as expected
+-- OK:
+
+CREATE TABLE cities (name text, population float, altitude int);
+CREATE TABLE capitals (state char(2)) INHERITS (cities);
+DROP TABLE capitals;
+DROP TABLE cities;
+
+-- OK:
+
+CREATE TABLE cities2 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals2 (state char(2)) INHERITS (cities2);
+INSERT INTO capitals2 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals2 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals2;
+SELECT * FROM cities2;
+DELETE FROM cities2 WHERE name = 'Moscow';
+SELECT * FROM capitals2;
+SELECT * FROM cities2;
+DROP TABLE capitals2;
+DROP TABLE cities2;
+
+-- ERROR:
+
+CREATE FAST TEMPORARY TABLE cities3 (name text, population float, altitude int);
+-- cannot inherit from temporary relation "cities3"
+CREATE TABLE capitals3 (state char(2)) INHERITS (cities3);
+DROP TABLE cities3;
+
+-- OK:
+
+CREATE FAST TEMPORARY TABLE cities4 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals4 (state char(2)) INHERITS (cities4);
+INSERT INTO capitals4 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals4 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals4;
+SELECT * FROM cities4;
+DELETE FROM cities4 WHERE name = 'Moscow';
+SELECT * FROM capitals4;
+SELECT * FROM cities4;
+DROP TABLE capitals4;
+DROP TABLE cities4;
+
+-- OK:
+
+CREATE TEMPORARY TABLE cities5 (name text, population float, altitude int);
+CREATE FAST TEMPORARY TABLE capitals5 (state char(2)) INHERITS (cities5);
+INSERT INTO capitals5 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals5 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals5;
+SELECT * FROM cities5;
+DELETE FROM cities5 WHERE name = 'Moscow';
+SELECT * FROM capitals5;
+SELECT * FROM cities5;
+DROP TABLE capitals5;
+DROP TABLE cities5;
+
+-- OK:
+
+CREATE FAST TEMPORARY TABLE cities6 (name text, population float, altitude int);
+CREATE TEMPORARY TABLE capitals6 (state char(2)) INHERITS (cities6);
+INSERT INTO capitals6 VALUES ('Moscow', 123.45, 789, 'RU');
+INSERT INTO capitals6 VALUES ('Paris', 543.21, 987, 'FR');
+SELECT * FROM capitals6;
+SELECT * FROM cities6;
+DELETE FROM cities6 WHERE name = 'Moscow';
+SELECT * FROM capitals6;
+SELECT * FROM cities6;
+DROP TABLE capitals6;
+DROP TABLE cities6;
+
+-- test index-only scan
+
+CREATE FAST TEMP TABLE fasttab_unique_prefix_beta(x int);
+CREATE TABLE fasttab_unique_prefix_alpha(x int);
+CREATE FAST TEMP TABLE fasttab_unique_prefix_delta(x int);
+CREATE TABLE fasttab_unique_prefix_epsilon(x int);
+CREATE TABLE fasttab_unique_prefix_gamma(x int);
+SELECT relname FROM pg_class WHERE relname > 'fasttab_unique_prefix_' ORDER BY relname LIMIT 5;
+DROP TABLE fasttab_unique_prefix_alpha;
+DROP TABLE fasttab_unique_prefix_beta;
+DROP TABLE fasttab_unique_prefix_gamma;
+DROP TABLE fasttab_unique_prefix_delta;
+DROP TABLE fasttab_unique_prefix_epsilon;
+
+-- test VACUUM / VACUUM FULL
+
+VACUUM;
+VACUUM FULL;
+SELECT * FROM fast_table_1;
+
+-- test ANALYZE
+
+CREATE FAST TEMP TABLE fasttab_analyze_test(x int, s text);
+INSERT INTO fasttab_analyze_test SELECT x, '--> ' || x FROM generate_series(1,100) as x;
+ANALYZE fasttab_analyze_test;
+SELECT count(*) FROM pg_statistic WHERE starelid = (SELECT oid FROM pg_class WHERE relname = 'fasttab_analyze_test');
+DROP TABLE fasttab_analyze_test;
+SELECT count(*) FROM pg_statistic WHERE starelid = (SELECT oid FROM pg_class WHERE relname = 'fasttab_analyze_test');
+
+-- cleanup after load test
+
+do $$
+declare
+  count_fast_table integer = 150;
+  t_sql text;
+begin
+  for i in 1 .. count_fast_table
+  loop
+    t_sql = 'DROP TABLE fast_table_' || i || ';';
+    execute t_sql;
+  end loop;
+end $$;
+
#12Geoff Winkless
pgsqladmin@geoff.dj
In reply to: Tomas Vondra (#4)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 30 July 2016 at 13:42, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

I'd argue that if you mess with catalogs directly, you're on your own.

Interesting. What would you suggest people use instead?

Geoff

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Robert Haas
robertmhaas@gmail.com
In reply to: Aleksander Alekseev (#11)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Thu, Aug 4, 2016 at 8:14 AM, Aleksander Alekseev
<a.alekseev@postgrespro.ru> wrote:

Thanks everyone for your remarks and comments!

Here is an improved version of a patch.

Main changes:
* Patch passes `make installcheck`
* Code is fully commented, also no more TODO's

I wish I sent this version of a patch last time. Now I realize it was
really hard to read and understand. Hope I managed to correct this
flaw. If you believe that some parts of the code are still poorly
commented or could be improved in any other way please let me know.

And as usual, any other comments, remarks or questions are highly
appreciated!

Three general comments:

1. It's always seemed to me that a huge problem with anything of this
sort is dependencies. For example, suppose I create a fast temporary
table and then I create a functional index on the fast temporary table
that uses some SQL function defined in pg_proc. Then, another user
drops the function. Then, I try to use the index. With regular
temporary tables, dependencies protect us here, but if there are no
catalog entries, I wonder how this can ever be made safe. Similar
problems exist for triggers, constraints, RLS policies, and attribute
defaults.

2. This inserts additional code in a bunch of really low-level places
like heap_hot_search_buffer, heap_update, heap_delete, etc. I think
what you've basically done here is create a new, in-memory heap AM and
then, because we don't have an API for adding new storage managers,
you've bolted it onto the existing heapam layer. That's certainly a
reasonable approach for creating a PoC, but I think we actually need a
real API here. Otherwise, when the next person comes along and wants
to add a third heap implementation, they've got to modify all of these
same places again. I don't think this code is reasonably maintainable
in this form.

3. Currently, temporary tables are parallel-restricted: a query that
references temporary tables can use parallelism, but access to the
temporary tables is only possible from within the leader. I suspect,
although I'm not entirely sure, that lifting this restriction would be
easier with our current temporary table implementation than with this
one, because the current temporary table implementation mostly relies
on a set of buffers that could be moved from backend-private memory to
DSM. On a quick look, this implementation uses a bunch of new data
structures that are heavy on pointers, so that gets quite a bit more
complicated to make parallel-safe (unless we adopt threads instead of
processes!).

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#14Dmitry Dolgov
9erthalion6@gmail.com
In reply to: Robert Haas (#13)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

​Hi

I tried to dig into this patch (which seems pretty interesting) to help
bring
it in good shape. Here are few random notes, I hope they can be helpful:

I think we actually need a real API here.

Definitely, there are plenty places in the new code with the same pattern:
* figure out if it's an action related to the fast temporary tables based
on
`ItemPointer`/relation OID/etc
* if it is, add some extra logic or skip something in original
implementation

in `heapam.c`, `indexam.c`, `xact.c`, `dependency.c`. I believe it's
possible to
make it more generic (although it will contain almost the same logic), e.g.
separate regular and fasttable implementations completely, and decide which
one
we should choose in that particular case.

Btw, I'm wondering about the `heap_*` functions in `heapam.c` - some of
them are
wrapped and never used directly, although they're contains in
`INTERFACE_ROUTINES` (like `simple_heap_delete` -> `heap_delete`), some of
them
aren't. It looks like inconsistency in terms of function names, probably it
should be unified?

What is needed is an overview of the approach, so that the reviewers can

read

that first,

I feel lack of such information even in new version of this patch (but, I'm
probably a noob in these matters). I noted that the `fasttab.c` file
contains some
general overview, but in terms of "what are we doing", and "why are we doing
this". I think general overview of "how are we doing this" also may be
useful.
And there are several slightly obvious commentaries like:

```
+ /* Is it a virtual TID? */
+ if (IsFasttabItemPointer(tid))
```

but I believe an introduction of a new API (see the previous note) will
solve
this eventually.

Why do we need the new relpersistence value? ISTM we could easily got with
just RELPERSISTENCE_TEMP, which would got right away of many chances as

the

steps are exactly the same.

I agree, it looks like `RELPERSISTENCE_FAST_TEMP` hasn't any influence on
the
code.

For example, suppose I create a fast temporary table and then I create a
functional index on the fast temporary table that uses some SQL function
defined in pg_proc.

Just to clarify, did you mean something like this?
```
create fast temp table fasttab(x int, s text);
create or replace function test_function_for_index(t text) returns text as
$$
begin
return lower(t);
end;
$$ language plpgsql immutable;
create index fasttab_s_idx on fasttab (test_function_for_index(s));
drop function test_function_for_index(t text);
```
As far as I understand dependencies should protect in case of fasttable too,
because everything is visible as in regular case, isn't it?

And finally one more question, why items of `FasttabIndexMethodsTable[]`
like
this one:
```
+ /* 2187, non-unique */
+ {InheritsParentIndexId, 1,
+ {Anum_pg_inherits_inhparent, 0, 0},
+ {CompareOid, CompareInvalid, CompareInvalid}
+ },
```
have this commentary before them? I assume it's an id and an extra
information,
and I'm concerned that they can easily become outdated inside commentary
block.
#15Robert Haas
robertmhaas@gmail.com
In reply to: Dmitry Dolgov (#14)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Sat, Aug 6, 2016 at 4:05 AM, Dmitry Dolgov <9erthalion6@gmail.com> wrote:

For example, suppose I create a fast temporary table and then I create a
functional index on the fast temporary table that uses some SQL function
defined in pg_proc.

Just to clarify, did you mean something like this?
```
create fast temp table fasttab(x int, s text);
create or replace function test_function_for_index(t text) returns text as
$$
begin
return lower(t);
end;
$$ language plpgsql immutable;
create index fasttab_s_idx on fasttab (test_function_for_index(s));
drop function test_function_for_index(t text);
```
As far as I understand dependencies should protect in case of fasttable too,
because everything is visible as in regular case, isn't it?

I think the whole idea of a fast temporary table is that there are no
catalog entries. If there are no catalog entries, then dependencies
are not visible. If there ARE catalog entries, to what do they refer?
Without a pg_class entry for the table, there's no table OID upon
which to depend.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#16Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#15)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Robert Haas <robertmhaas@gmail.com> writes:

I think the whole idea of a fast temporary table is that there are no
catalog entries. If there are no catalog entries, then dependencies
are not visible. If there ARE catalog entries, to what do they refer?
Without a pg_class entry for the table, there's no table OID upon
which to depend.

TBH, I think that the chances of such a design getting committed are
not distinguishable from zero. Tables have to have OIDs; there is just
too much code that assumes that. And I seriously doubt that it will
work (for any large value of "work") without catalog entries.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#17Anastasia Lubennikova
a.lubennikova@postgrespro.ru
In reply to: Robert Haas (#13)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

05.08.2016 19:41, Robert Haas:

2. This inserts additional code in a bunch of really low-level places
like heap_hot_search_buffer, heap_update, heap_delete, etc. I think
what you've basically done here is create a new, in-memory heap AM and
then, because we don't have an API for adding new storage managers,
you've bolted it onto the existing heapam layer. That's certainly a
reasonable approach for creating a PoC, but I think we actually need a
real API here. Otherwise, when the next person comes along and wants
to add a third heap implementation, they've got to modify all of these
same places again. I don't think this code is reasonably maintainable
in this form.

As I can see, you recommend to clean up the API of storage
management code. I strongly agree that it's high time to do it.

So, I started the discussion about refactoring and improving API
of heapam and heap relations.
You can find it on commitfest:
https://commitfest.postgresql.org/10/700/

I'll be glad to see your thoughts on the thread.

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

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#18Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#16)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 2016-08-07 14:46:06 -0400, Tom Lane wrote:

Robert Haas <robertmhaas@gmail.com> writes:

I think the whole idea of a fast temporary table is that there are no
catalog entries. If there are no catalog entries, then dependencies
are not visible. If there ARE catalog entries, to what do they refer?
Without a pg_class entry for the table, there's no table OID upon
which to depend.

TBH, I think that the chances of such a design getting committed are
not distinguishable from zero. Tables have to have OIDs; there is just
too much code that assumes that. And I seriously doubt that it will
work (for any large value of "work") without catalog entries.

That seems a bit too defeatist. It's obviously not a small change to get
there - and I don't think the patch upthread is really attacking the
relevant problems yet - but saying that we'll never have temp tables
without pg_class/pg_depend bloat seems to be pretty close to just giving
up. Having 8 byte oids (as explicit columns instead of magic? Or just
oid64?) and then reserving ranges for temp objects stored in a local
memory seems to be feasible. The pinning problem could potentially be
solved by "session lifetime" pins in pg_depend, which prevents dependent
objects being dropped. Obviously that's just spitballing; but I think
the problem is too big to just give up.

Andres

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#19Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#18)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Andres Freund <andres@anarazel.de> writes:

On 2016-08-07 14:46:06 -0400, Tom Lane wrote:

Robert Haas <robertmhaas@gmail.com> writes:

I think the whole idea of a fast temporary table is that there are no
catalog entries. If there are no catalog entries, then dependencies
are not visible. If there ARE catalog entries, to what do they refer?
Without a pg_class entry for the table, there's no table OID upon
which to depend.

TBH, I think that the chances of such a design getting committed are
not distinguishable from zero. Tables have to have OIDs; there is just
too much code that assumes that. And I seriously doubt that it will
work (for any large value of "work") without catalog entries.

That seems a bit too defeatist.

Huh? I didn't say we shouldn't work on the problem --- I just think that
this particular approach isn't good. Which you seemed to agree with.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#20Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#19)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 2016-08-14 21:04:57 -0400, Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

On 2016-08-07 14:46:06 -0400, Tom Lane wrote:

Robert Haas <robertmhaas@gmail.com> writes:

I think the whole idea of a fast temporary table is that there are no
catalog entries. If there are no catalog entries, then dependencies
are not visible. If there ARE catalog entries, to what do they refer?
Without a pg_class entry for the table, there's no table OID upon
which to depend.

TBH, I think that the chances of such a design getting committed are
not distinguishable from zero. Tables have to have OIDs; there is just
too much code that assumes that. And I seriously doubt that it will
work (for any large value of "work") without catalog entries.

That seems a bit too defeatist.

Huh? I didn't say we shouldn't work on the problem --- I just think that
this particular approach isn't good. Which you seemed to agree with.

I took your statement to mean that they need a pg_class entry - even if
there were a partial solution to the pg_depend problem allowing to avoid
pg_attribute entries, tha't still not really be a solution. If that's
not what you mean, sorry - and nice that we agree ;)

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#21Aleksander Alekseev
a.alekseev@postgrespro.ru
In reply to: Andres Freund (#20)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

I think the whole idea of a fast temporary table is that there
are no catalog entries. If there are no catalog entries, then
dependencies are not visible. If there ARE catalog entries, to
what do they refer? Without a pg_class entry for the table,
there's no table OID upon which to depend.

TBH, I think that the chances of such a design getting committed
are not distinguishable from zero. Tables have to have OIDs;
there is just too much code that assumes that. And I seriously
doubt that it will work (for any large value of "work") without
catalog entries.

That seems a bit too defeatist.

Huh? I didn't say we shouldn't work on the problem --- I just
think that this particular approach isn't good. Which you seemed
to agree with.

I took your statement to mean that they need a pg_class entry - even
if there were a partial solution to the pg_depend problem allowing to
avoid pg_attribute entries, tha't still not really be a solution. If
that's not what you mean, sorry - and nice that we agree ;)

Just to keep things sane I would like to remind that in this concrete
patch there _are_ catalog entries:

```
[...]
This file contents imlementation of special type of temporary tables ---
fast temporary tables (FTT). From user perspective they work exactly as
regular temporary tables. However there are no records about FTTs in
pg_catalog. These records are stored in backend's memory instead and
mixed with regular records during scans of catalog tables. We refer to
corresponding tuples of catalog tables as "in-memory" or "virtual"
tuples and to all these tuples together --- as "in-memory" or "virtual"
catalog.
[...]
```

As Tom pointed out a lot of PL/pgSQL code would stop working otherwise.
Also I mentioned that in this case even \d and \d+ would not work.

I personally find this discussion very confusing. Maybe we should
concentrate on a concrete patch instead of some abstract ideas, and
topics that are still open.

For instance it surprises me that apparently there is no one who
objects "lets make all temporary tables fast temporary tables" idea.
Since in this case code would use more memory for keeping a virtual
catalog wouldn't it be considered a major change of behavior that can
break someones production environment?

--
Best regards,
Aleksander Alekseev

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Pavel Stehule
pavel.stehule@gmail.com
In reply to: Aleksander Alekseev (#8)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

2016-08-15 11:01 GMT+02:00 Aleksander Alekseev <a.alekseev@postgrespro.ru>:

I think the whole idea of a fast temporary table is that there
are no catalog entries. If there are no catalog entries, then
dependencies are not visible. If there ARE catalog entries, to
what do they refer? Without a pg_class entry for the table,
there's no table OID upon which to depend.

TBH, I think that the chances of such a design getting committed
are not distinguishable from zero. Tables have to have OIDs;
there is just too much code that assumes that. And I seriously
doubt that it will work (for any large value of "work") without
catalog entries.

That seems a bit too defeatist.

Huh? I didn't say we shouldn't work on the problem --- I just
think that this particular approach isn't good. Which you seemed
to agree with.

I took your statement to mean that they need a pg_class entry - even
if there were a partial solution to the pg_depend problem allowing to
avoid pg_attribute entries, tha't still not really be a solution. If
that's not what you mean, sorry - and nice that we agree ;)

Just to keep things sane I would like to remind that in this concrete
patch there _are_ catalog entries:

```
[...]
This file contents imlementation of special type of temporary tables ---
fast temporary tables (FTT). From user perspective they work exactly as
regular temporary tables. However there are no records about FTTs in
pg_catalog. These records are stored in backend's memory instead and
mixed with regular records during scans of catalog tables. We refer to
corresponding tuples of catalog tables as "in-memory" or "virtual"
tuples and to all these tuples together --- as "in-memory" or "virtual"
catalog.
[...]
```

As Tom pointed out a lot of PL/pgSQL code would stop working otherwise.
Also I mentioned that in this case even \d and \d+ would not work.

I personally find this discussion very confusing. Maybe we should
concentrate on a concrete patch instead of some abstract ideas and
topics that are still open.

For instance it surprises me that apparently there is no one who
objects "lets make all temporary tables fast temporary tables" idea.
Since in this case code would use more memory for keeping a virtual
catalog wouldn't it be considered a major change of behavior that could
break someones production environment?

It is pretty hard discussion about cost or usability of FTT. The small FTT
(for usage in PLpgSQL) can be replaced by arrays. The overhead of
pg_catalog of big TT is not significant. So introduction special
proprietary table type is debatable.

Probably size of metadata of temporary tables should be minimal - currently
all metadata are cached in memory - and it is not a problem.

But we can change this discussion little bit different. I believe so
solution should be *global temporary tables*. These tables has persistent
catalogue entries. Data are joined with session. These tables can be
effective solution of problem with temporary tables, can be strong benefit
for developers (more comfortable, possible static analyse of PLpgSQL) and
it simplify life to all people who has do migration from Oracle. So only
benefits are there :).

Regards

Pavel

Show quoted text

--
Best regards,
Aleksander Alekseev

#23Aleksander Alekseev
a.alekseev@postgrespro.ru
In reply to: Pavel Stehule (#22)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

But we can change this discussion little bit different. I believe so
solution should be *global temporary tables*. These tables has
persistent catalogue entries. Data are joined with session. These
tables can be effective solution of problem with temporary tables,
can be strong benefit for developers (more comfortable, possible
static analyse of PLpgSQL) and it simplify life to all people who has
do migration from Oracle. So only benefits are there :).

I don't think that global temporary tables solve "catalog bloating that
causes auto vacuum" problem. I suggest we don't change a topic. Or maybe
I don't know something about global temporary tables?

--
Best regards,
Aleksander Alekseev

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#24Pavel Stehule
pavel.stehule@gmail.com
In reply to: Aleksander Alekseev (#23)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

2016-08-15 12:00 GMT+02:00 Aleksander Alekseev <a.alekseev@postgrespro.ru>:

But we can change this discussion little bit different. I believe so
solution should be *global temporary tables*. These tables has
persistent catalogue entries. Data are joined with session. These
tables can be effective solution of problem with temporary tables,
can be strong benefit for developers (more comfortable, possible
static analyse of PLpgSQL) and it simplify life to all people who has
do migration from Oracle. So only benefits are there :).

I don't think that global temporary tables solve "catalog bloating that
causes auto vacuum" problem. I suggest we don't change a topic. Or maybe
I don't know something about global temporary tables?

The global temporary tables has persistent rows in the catalogue. The
mapping to files can be marked as special and real mapping should be only
in memory.

So the changes in catalogue related to global temporary tables are pretty
less frequently.

Regards

Pavel

Show quoted text

--
Best regards,
Aleksander Alekseev

#25Aleksander Alekseev
a.alekseev@postgrespro.ru
In reply to: Pavel Stehule (#24)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

The global temporary tables has persistent rows in the catalogue. The
mapping to files can be marked as special and real mapping should be
only in memory.

So the changes in catalogue related to global temporary tables are
pretty less frequently.

I'm afraid I still don't get it. Let say I have an application that
does `CREATE TEMP TABLE xx ; DROP TABLE xx` in every session all the
time. Naturally there is not only one temp table per session. Could you
explain please in more detail how exactly do these persistent rows help?

--
Best regards,
Aleksander Alekseev

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#26Pavel Stehule
pavel.stehule@gmail.com
In reply to: Aleksander Alekseev (#25)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

2016-08-15 12:18 GMT+02:00 Aleksander Alekseev <a.alekseev@postgrespro.ru>:

The global temporary tables has persistent rows in the catalogue. The
mapping to files can be marked as special and real mapping should be
only in memory.

So the changes in catalogue related to global temporary tables are
pretty less frequently.

I'm afraid I still don't get it. Let say I have an application that
does `CREATE TEMP TABLE xx ; DROP TABLE xx` in every session all the
time. Naturally there is not only one temp table per session. Could you
explain please in more detail how exactly do these persistent rows help?

when you use global temporary tables, then you create it only once - like
usual tables.

you don't drop these tables.

Regards

Pavel

Show quoted text

--
Best regards,
Aleksander Alekseev

#27Christoph Berg
myon@debian.org
In reply to: Tom Lane (#5)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Re: Tom Lane 2016-07-30 <1184.1469890030@sss.pgh.pa.us>

In short, I think that the way to make something like this work is to
figure out how to have "virtual" catalog rows describing a temp table.
Or maybe to partition the catalogs so that vacuuming away temp-table
rows is easier/cheaper than today.

We should also be thinking about how the opposite idea of "global"
temp tables (I believe that's what Oracle calls them) would work.
These have a persistent structure in the catalogs, just the data is
private to every session (or transaction); every connection starts
with an empty temp table and for their use.

I'd guess that type of global temp tables would fix the bloat problem
also very efficiently. (Ad-hoc temp tables shouldn't occur that often
so the bloat caused by them wouldn't matter that much. If they do,
their structure is likely always the same, and they could be made
"global" in the schema.)

The bit that needs to be thought out here would be how to maintain
statistics for these tables. Obviously ANALYZE shouldn't update any
globally visible data.

Christoph

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#28Christoph Berg
myon@debian.org
In reply to: Christoph Berg (#27)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Re: To Tom Lane 2016-08-15 <20160815111057.v2mqqjp4aabvwqnc@msg.df7cb.de>

Re: Tom Lane 2016-07-30 <1184.1469890030@sss.pgh.pa.us>

In short, I think that the way to make something like this work is to
figure out how to have "virtual" catalog rows describing a temp table.
Or maybe to partition the catalogs so that vacuuming away temp-table
rows is easier/cheaper than today.

We should also be thinking about how the opposite idea of "global"
temp tables

(Obviously I should catch up on the rest of the thread when postponing
a message for an hour or two. Sorry for the duplicated idea here...)

Christoph

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#29Robert Haas
robertmhaas@gmail.com
In reply to: Aleksander Alekseev (#8)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Why are you sending this off-list? Please let's keep the discussion
on the mailing list. I suggest resending this there.

On Mon, Aug 15, 2016 at 5:01 AM, Aleksander Alekseev
<a.alekseev@postgrespro.ru> wrote:

I think the whole idea of a fast temporary table is that there
are no catalog entries. If there are no catalog entries, then
dependencies are not visible. If there ARE catalog entries, to
what do they refer? Without a pg_class entry for the table,
there's no table OID upon which to depend.

TBH, I think that the chances of such a design getting committed
are not distinguishable from zero. Tables have to have OIDs;
there is just too much code that assumes that. And I seriously
doubt that it will work (for any large value of "work") without
catalog entries.

That seems a bit too defeatist.

Huh? I didn't say we shouldn't work on the problem --- I just
think that this particular approach isn't good. Which you seemed
to agree with.

I took your statement to mean that they need a pg_class entry - even
if there were a partial solution to the pg_depend problem allowing to
avoid pg_attribute entries, tha't still not really be a solution. If
that's not what you mean, sorry - and nice that we agree ;)

Just to keep things sane I would like to remind that in this concrete
patch there _are_ catalog entries:

```
[...]
This file contents imlementation of special type of temporary tables ---
fast temporary tables (FTT). From user perspective they work exactly as
regular temporary tables. However there are no records about FTTs in
pg_catalog. These records are stored in backend's memory instead and
mixed with regular records during scans of catalog tables. We refer to
corresponding tuples of catalog tables as "in-memory" or "virtual"
tuples and to all these tuples together --- as "in-memory" or "virtual"
catalog.
[...]
```

As Tom pointed out a lot of PL/pgSQL code would stop working otherwise.
Also I mentioned that in this case even \d and \d+ would not work.

I personally find this discussion very confusing. Maybe we should
concentrate on a concrete patch instead of some abstract ideas and
topics that are still open.

For instance it surprises me that apparently there is no one who
objects "lets make all temporary tables fast temporary tables" idea.
Since in this case code would use more memory for keeping a virtual
catalog wouldn't it be considered a major change of behavior that could
break someones production environment?

--
Best regards,
Aleksander Alekseev

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#30Aleksander Alekseev
a.alekseev@postgrespro.ru
In reply to: Robert Haas (#29)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Why are you sending this off-list? Please let's keep the discussion
on the mailing list. I suggest resending this there.

Sorry for that. I accidentally removed pgsql-hackers@ from CC list or
maybe my email client somehow did it for me. Short after that I realized
my mistake and sent a copy to the mailing list.

--
Best regards,
Aleksander Alekseev

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#31Robert Haas
robertmhaas@gmail.com
In reply to: Aleksander Alekseev (#21)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Mon, Aug 15, 2016 at 5:12 AM, Aleksander Alekseev
<a.alekseev@postgrespro.ru> wrote:

Just to keep things sane I would like to remind that in this concrete
patch there _are_ catalog entries:

```
[...]
This file contents imlementation of special type of temporary tables ---
fast temporary tables (FTT). From user perspective they work exactly as
regular temporary tables. However there are no records about FTTs in
pg_catalog. These records are stored in backend's memory instead and
mixed with regular records during scans of catalog tables. We refer to
corresponding tuples of catalog tables as "in-memory" or "virtual"
tuples and to all these tuples together --- as "in-memory" or "virtual"
catalog.
[...]
```

That doesn't really solve the problem, because OTHER backends won't be
able to see them. So, if I create a fast temporary table in one
session that depends on a permanent object, some other session can
drop the permanent object. If there were REAL catalog entries, that
wouldn't work, because the other session would see the dependency.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#32Jim Nasby
Jim.Nasby@BlueTreble.com
In reply to: Robert Haas (#31)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 8/16/16 11:59 AM, Robert Haas wrote:
...

That doesn't really solve the problem, because OTHER backends won't be
able to see them. So, if I create a fast temporary table in one
session that depends on a permanent object, some other session can
drop the permanent object. If there were REAL catalog entries, that
wouldn't work, because the other session would see the dependency.

Some discussion about TEMP functions is happening on -general right now,
and there's other things where temp objects are good to have, so it'd be
nice to have a more generic fix for this stuff. Is the idea of
"partitioning" the catalogs to store temp objects separate from
permanent fatally flawed?
--
Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
Experts in Analytics, Data Architecture and PostgreSQL
Data in Trouble? Get it in Treble! http://BlueTreble.com
855-TREBLE2 (855-873-2532) mobile: 512-569-9461

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#33Aleksander Alekseev
a.alekseev@postgrespro.ru
In reply to: Robert Haas (#31)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

That doesn't really solve the problem, because OTHER backends won't be
able to see them. So, if I create a fast temporary table in one
session that depends on a permanent object, some other session can
drop the permanent object. If there were REAL catalog entries, that
wouldn't work, because the other session would see the dependency.

This is a good point. However current implementation doesn't allow to
do that. There is a related bug though, a minor one.

In session 1:

```
CREATE TABLE cities2 (name text, population float, altitude int);
CREATE FAST TEMPORARY TABLE capitals2 (state char(2)) INHERITS (cities2);
```

In session 2:

```
DROP TABLE cities2;

ERROR: cache lookup failed for relation 16401
```

Instead of "cache lookup failed" probably a better error message
should be displayed. Something like "cannot drop table cities2 because
other objects depend on it". I will send a corrected patch shortly.

Everything else seems to work as expected.

If you discover any other bugs please let me know!

--
Best regards,
Aleksander Alekseev

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#34Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Aleksander Alekseev (#33)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/17/2016 11:50 AM, Aleksander Alekseev wrote:

That doesn't really solve the problem, because OTHER backends won't be
able to see them. So, if I create a fast temporary table in one
session that depends on a permanent object, some other session can
drop the permanent object. If there were REAL catalog entries, that
wouldn't work, because the other session would see the dependency.

This is a good point. However current implementation doesn't allow to
do that.

IMHO without handling that, the design is effectively broken and has
very little change (or rather none at all) to get committed.

I think one way to fix that would be to store the virtual tuples in
shared memory (instead of process memory). That will certainly require
locking and synchronization, but well - it needs to be shared.

There is a related bug though, a minor one.

In session 1:

```
CREATE TABLE cities2 (name text, population float, altitude int);
CREATE FAST TEMPORARY TABLE capitals2 (state char(2)) INHERITS (cities2);
```

In session 2:

```
DROP TABLE cities2;

ERROR: cache lookup failed for relation 16401
```

Instead of "cache lookup failed" probably a better error message
should be displayed. Something like "cannot drop table cities2
because other objects depend on it". I will send a corrected patch
shortly.

Everything else seems to work as expected.

If you discover any other bugs please let me know!

While a better error message would be nice, this is curing the symptoms
and not the cause. I think a proper design needs to prevent the DROP by
using dependencies.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#35Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Pavel Stehule (#26)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/15/2016 12:23 PM, Pavel Stehule wrote:

2016-08-15 12:18 GMT+02:00 Aleksander Alekseev
<a.alekseev@postgrespro.ru <mailto:a.alekseev@postgrespro.ru>>:

The global temporary tables has persistent rows in the catalogue. The
mapping to files can be marked as special and real mapping should be
only in memory.

So the changes in catalogue related to global temporary tables are
pretty less frequently.

I'm afraid I still don't get it. Let say I have an application that
does `CREATE TEMP TABLE xx ; DROP TABLE xx` in every session all the
time. Naturally there is not only one temp table per session. Could you
explain please in more detail how exactly do these persistent rows help?

when you use global temporary tables, then you create it only once -
like usual tables.

you don't drop these tables.

I share the view that this is a better/simpler solution to the problem.
It will still require virtual (in-memory) tuples for pg_statistic
records, but everything else works pretty much as for regular tables. In
particular there are no problems with dependencies.

The obvious disadvantage is that it requires changes to applications.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#36Pavel Stehule
pavel.stehule@gmail.com
In reply to: Tomas Vondra (#35)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

2016-08-18 16:33 GMT+02:00 Tomas Vondra <tomas.vondra@2ndquadrant.com>:

On 08/15/2016 12:23 PM, Pavel Stehule wrote:

2016-08-15 12:18 GMT+02:00 Aleksander Alekseev
<a.alekseev@postgrespro.ru <mailto:a.alekseev@postgrespro.ru>>:

The global temporary tables has persistent rows in the catalogue.

The

mapping to files can be marked as special and real mapping should be
only in memory.

So the changes in catalogue related to global temporary tables are
pretty less frequently.

I'm afraid I still don't get it. Let say I have an application that
does `CREATE TEMP TABLE xx ; DROP TABLE xx` in every session all the
time. Naturally there is not only one temp table per session. Could
you
explain please in more detail how exactly do these persistent rows
help?

when you use global temporary tables, then you create it only once -
like usual tables.

you don't drop these tables.

I share the view that this is a better/simpler solution to the problem. It
will still require virtual (in-memory) tuples for pg_statistic records, but
everything else works pretty much as for regular tables. In particular
there are no problems with dependencies.

The obvious disadvantage is that it requires changes to applications.

sure - as plpgsql developer I can say, the global temp tables are much more
friendly - so rewriting in application is enjoy work.

Regards

Pavel

Show quoted text

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#37Robert Haas
robertmhaas@gmail.com
In reply to: Jim Nasby (#32)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Tue, Aug 16, 2016 at 8:03 PM, Jim Nasby <Jim.Nasby@bluetreble.com> wrote:

On 8/16/16 11:59 AM, Robert Haas wrote:
...

That doesn't really solve the problem, because OTHER backends won't be
able to see them. So, if I create a fast temporary table in one
session that depends on a permanent object, some other session can
drop the permanent object. If there were REAL catalog entries, that
wouldn't work, because the other session would see the dependency.

Some discussion about TEMP functions is happening on -general right now, and
there's other things where temp objects are good to have, so it'd be nice to
have a more generic fix for this stuff. Is the idea of "partitioning" the
catalogs to store temp objects separate from permanent fatally flawed?

I wouldn't say it's fatally flawed. But you might need a
world-renowned team of physicians working round the clock for days in
a class 1 trauma center to save it. If you imagine that you have a
permanent pg_class which holds permanent tables and a temporary
pg_class per-backend which stores temporary tables, then you very
quickly end up with the same deadly flaw as in Aleksander's design:
other backends cannot see all of the dependency entries and can drop
things that they shouldn't be permitted to drop. However, you could
have a permanent pg_class which holds the records for permanent tables
and an *unlogged* table, say pg_class_unlogged, which holds records
for temporary tables. Now everybody can see everybody else's data,
yet we don't have to create permanent catalog entries. So we are not
dead. All of the temporary catalog tables vanish on a crash, too, and
in a very clean way, which is great.

However:

1. The number of tables for which we would need to add a duplicate,
unlogged table is formidable. You need pg_attribute, pg_attrdef,
pg_constraint, pg_description, pg_type, pg_trigger, pg_rewrite, etc.
And the backend changes needed so that we used the unlogged copy for
temp tables and the permanent copy for regular tables is probably
really large.

2. You can't write to unlogged tables on standby servers, so this
doesn't help solve the problem of wanting to use temporary tables on
standbys.

3. While it makes creating temporary tables a lighter-weight
operation, because you no longer need to write WAL for the catalog
entries, there's probably still substantially more overhead than just
stuffing them in backend-local RAM. So the performance benefits are
probably fairly modest.

Overall I feel like the development effort that it would take to make
this work would almost certainly be better-expended elsewhere. But of
course I'm not in charge of how people who work for other companies
spend their time...

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#38Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Robert Haas (#37)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Robert Haas wrote:

However:

1. The number of tables for which we would need to add a duplicate,
unlogged table is formidable. You need pg_attribute, pg_attrdef,
pg_constraint, pg_description, pg_type, pg_trigger, pg_rewrite, etc.
And the backend changes needed so that we used the unlogged copy for
temp tables and the permanent copy for regular tables is probably
really large.

Check. This is the most serious issue, IMV.

2. You can't write to unlogged tables on standby servers, so this
doesn't help solve the problem of wanting to use temporary tables on
standbys.

Check. We could think about relaxing this restriction, which would
enable the feature to satisfy that use case. (I think the main
complication there is the init fork of btrees on those catalogs; other
relations could just be truncated to empty on restart.)

3. While it makes creating temporary tables a lighter-weight
operation, because you no longer need to write WAL for the catalog
entries, there's probably still substantially more overhead than just
stuffing them in backend-local RAM. So the performance benefits are
probably fairly modest.

You also save catalog bloat ... These benefits may not be tremendous,
but I think they may be good enough for many users.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#39Andres Freund
andres@anarazel.de
In reply to: Alvaro Herrera (#38)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 2016-08-22 17:50:11 -0300, Alvaro Herrera wrote:

2. You can't write to unlogged tables on standby servers, so this
doesn't help solve the problem of wanting to use temporary tables on
standbys.

Check. We could think about relaxing this restriction, which would
enable the feature to satisfy that use case. (I think the main
complication there is the init fork of btrees on those catalogs; other
relations could just be truncated to empty on restart.)

Isn't the main complication that visibility currently requires xids to
be assigned?

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#40Aleksander Alekseev
a.alekseev@postgrespro.ru
In reply to: Andres Freund (#39)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Thank you everyone for great comments!

have a permanent pg_class which holds the records for permanent tables
and an *unlogged* table, say pg_class_unlogged, which holds records
for temporary tables. Now everybody can see everybody else's data,
yet we don't have to create permanent catalog entries. So we are not
dead. All of the temporary catalog tables vanish on a crash, too, and
in a very clean way, which is great.

[...]

Overall I feel like the development effort that it would take to make
this work would almost certainly be better-expended elsewhere.

Agree. This is an interesting idea but considering named drawbacks,
especially:

2. You can't write to unlogged tables on standby servers, so this
doesn't help solve the problem of wanting to use temporary tables on
standbys.

... I don't think it's worth an effort.

when you use global temporary tables, then you create it only once -
like usual tables.

you don't drop these tables.

I share the view that this is a better/simpler solution to the problem.
It will still require virtual (in-memory) tuples for pg_statistic
records, but everything else works pretty much as for regular tables. In
particular there are no problems with dependencies.

The obvious disadvantage is that it requires changes to applications.

Frankly I have much more faith in Tom's idea of using virtual part of the
catalog for all temporary tables, i.e turning all temporary tables into
"fast" temporary tables. Instead of introducing a new type of temporary tables
that solve catalog bloating problem and forcing users to rewrite applications
why not just not to create a problem in a first place?

I think one way to fix that would be to store the virtual tuples in
shared memory (instead of process memory). That will certainly require
locking and synchronization, but well - it needs to be shared.

I believe currently this is the most promising course of action. In first
implementation we could just place all virtual part of the catalog in a shared
memory and protect it with a single lock. If it will work as expected the next
step would be elimination of bottlenecks --- using multiple locks, moving part
of a virtual catalog to local backend's memory, etc.

As always, please don't hesitate to share any thoughts on this topic!

--
Best regards,
Aleksander Alekseev

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#41Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Robert Haas (#37)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/22/2016 10:32 PM, Robert Haas wrote:

...

1. The number of tables for which we would need to add a duplicate,
unlogged table is formidable. You need pg_attribute, pg_attrdef,
pg_constraint, pg_description, pg_type, pg_trigger, pg_rewrite, etc.
And the backend changes needed so that we used the unlogged copy for
temp tables and the permanent copy for regular tables is probably
really large.

2. You can't write to unlogged tables on standby servers, so this
doesn't help solve the problem of wanting to use temporary tables on
standbys.

3. While it makes creating temporary tables a lighter-weight
operation, because you no longer need to write WAL for the catalog
entries, there's probably still substantially more overhead than just
stuffing them in backend-local RAM. So the performance benefits are
probably fairly modest.

Overall I feel like the development effort that it would take to make
this work would almost certainly be better-expended elsewhere. But of
course I'm not in charge of how people who work for other companies
spend their time...

Could someone please explain how the unlogged tables are supposed to fix
the catalog bloat problem, as stated in the initial patch submission?
We'd still need to insert/delete the catalog rows when creating/dropping
the temporary tables, causing the bloat. Or is there something I'm missing?

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#42Claudio Freire
klaussfreire@gmail.com
In reply to: Tomas Vondra (#41)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Tue, Aug 23, 2016 at 7:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On 08/22/2016 10:32 PM, Robert Haas wrote:

...

1. The number of tables for which we would need to add a duplicate,
unlogged table is formidable. You need pg_attribute, pg_attrdef,
pg_constraint, pg_description, pg_type, pg_trigger, pg_rewrite, etc.
And the backend changes needed so that we used the unlogged copy for
temp tables and the permanent copy for regular tables is probably
really large.

2. You can't write to unlogged tables on standby servers, so this
doesn't help solve the problem of wanting to use temporary tables on
standbys.

3. While it makes creating temporary tables a lighter-weight
operation, because you no longer need to write WAL for the catalog
entries, there's probably still substantially more overhead than just
stuffing them in backend-local RAM. So the performance benefits are
probably fairly modest.

Overall I feel like the development effort that it would take to make
this work would almost certainly be better-expended elsewhere. But of
course I'm not in charge of how people who work for other companies
spend their time...

Could someone please explain how the unlogged tables are supposed to fix the
catalog bloat problem, as stated in the initial patch submission? We'd still
need to insert/delete the catalog rows when creating/dropping the temporary
tables, causing the bloat. Or is there something I'm missing?

Wouldn't more aggressive vacuuming of catalog tables fix the bloat?

Perhaps reserving a worker or N to run only on catalog schemas?

That'd be far simpler.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#43Andres Freund
andres@anarazel.de
In reply to: Claudio Freire (#42)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 2016-08-23 19:18:04 -0300, Claudio Freire wrote:

On Tue, Aug 23, 2016 at 7:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed to fix the
catalog bloat problem, as stated in the initial patch submission? We'd still
need to insert/delete the catalog rows when creating/dropping the temporary
tables, causing the bloat. Or is there something I'm missing?

Beats me.

Wouldn't more aggressive vacuuming of catalog tables fix the bloat?

Not really in my experience, at least not without more drastic vacuum
changes. The issue is that if you have a single "long running"
transaction (in some workloads that can even just be a 3 min taking
query/xact), nothing will be cleaned up during that time. If you have a
few hundred temp tables created per sec, you'll be in trouble even
then. Not to speak of the case where you have queries taking hours (say
a backup).

Andres

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#44Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Claudio Freire (#42)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/24/2016 12:18 AM, Claudio Freire wrote:

On Tue, Aug 23, 2016 at 7:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On 08/22/2016 10:32 PM, Robert Haas wrote:

...

1. The number of tables for which we would need to add a duplicate,
unlogged table is formidable. You need pg_attribute, pg_attrdef,
pg_constraint, pg_description, pg_type, pg_trigger, pg_rewrite, etc.
And the backend changes needed so that we used the unlogged copy for
temp tables and the permanent copy for regular tables is probably
really large.

2. You can't write to unlogged tables on standby servers, so this
doesn't help solve the problem of wanting to use temporary tables on
standbys.

3. While it makes creating temporary tables a lighter-weight
operation, because you no longer need to write WAL for the catalog
entries, there's probably still substantially more overhead than just
stuffing them in backend-local RAM. So the performance benefits are
probably fairly modest.

Overall I feel like the development effort that it would take to make
this work would almost certainly be better-expended elsewhere. But of
course I'm not in charge of how people who work for other companies
spend their time...

Could someone please explain how the unlogged tables are supposed to fix the
catalog bloat problem, as stated in the initial patch submission? We'd still
need to insert/delete the catalog rows when creating/dropping the temporary
tables, causing the bloat. Or is there something I'm missing?

Wouldn't more aggressive vacuuming of catalog tables fix the bloat?

Perhaps reserving a worker or N to run only on catalog schemas?

That'd be far simpler.

Maybe, although IIRC the issues with catalog bloat were due to a
combination of long queries and many temporary tables being
created/dropped. In that case simply ramping up autovacuum (or even
having a dedicated workers for catalogs) would not realy help due to the
xmin horizon being blocked by the long-running queries.

Maybe it's entirely crazy idea due to the wine I drank at the dinner,
but couldn't we vacuum the temporary table records differently? For
example, couldn't we just consider them removable as soon as the backend
that owns them disappears?

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#45Claudio Freire
klaussfreire@gmail.com
In reply to: Andres Freund (#43)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Tue, Aug 23, 2016 at 7:20 PM, Andres Freund <andres@anarazel.de> wrote:

Wouldn't more aggressive vacuuming of catalog tables fix the bloat?

Not really in my experience, at least not without more drastic vacuum
changes. The issue is that if you have a single "long running"
transaction (in some workloads that can even just be a 3 min taking
query/xact), nothing will be cleaned up during that time. If you have a
few hundred temp tables created per sec, you'll be in trouble even
then. Not to speak of the case where you have queries taking hours (say
a backup).

Well, my experience isn't as extreme as that (just a few dozen temp
tables per minute), but when I see bloat in catalog tables it's
because all autovacuum workers are stuck vacuuming huge tables for
huge periods of time (hours or days).

So that's certainly another bloat case to consider.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#46Andres Freund
andres@anarazel.de
In reply to: Claudio Freire (#45)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 2016-08-23 19:33:33 -0300, Claudio Freire wrote:

On Tue, Aug 23, 2016 at 7:20 PM, Andres Freund <andres@anarazel.de> wrote:

Wouldn't more aggressive vacuuming of catalog tables fix the bloat?

Not really in my experience, at least not without more drastic vacuum
changes. The issue is that if you have a single "long running"
transaction (in some workloads that can even just be a 3 min taking
query/xact), nothing will be cleaned up during that time. If you have a
few hundred temp tables created per sec, you'll be in trouble even
then. Not to speak of the case where you have queries taking hours (say
a backup).

Well, my experience isn't as extreme as that (just a few dozen temp
tables per minute), but when I see bloat in catalog tables it's
because all autovacuum workers are stuck vacuuming huge tables for
huge periods of time (hours or days).

Well, that's because our defaults are batshit stupid.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#47Claudio Freire
klaussfreire@gmail.com
In reply to: Tomas Vondra (#44)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Tue, Aug 23, 2016 at 7:25 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed to fix
the
catalog bloat problem, as stated in the initial patch submission? We'd
still
need to insert/delete the catalog rows when creating/dropping the
temporary
tables, causing the bloat. Or is there something I'm missing?

Wouldn't more aggressive vacuuming of catalog tables fix the bloat?

Perhaps reserving a worker or N to run only on catalog schemas?

That'd be far simpler.

Maybe, although IIRC the issues with catalog bloat were due to a combination
of long queries and many temporary tables being created/dropped. In that
case simply ramping up autovacuum (or even having a dedicated workers for
catalogs) would not realy help due to the xmin horizon being blocked by the
long-running queries.

Maybe it's entirely crazy idea due to the wine I drank at the dinner, but
couldn't we vacuum the temporary table records differently? For example,
couldn't we just consider them removable as soon as the backend that owns
them disappears?

Or perhaps go all the way and generalize that to rows that never
become visible outside their parent transaction.

As in, delete of rows created by the deleting transaction could clean
up, carefully to avoid voiding indexes and all that, but more
aggressively than regular deletes.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#48Greg Stark
stark@mit.edu
In reply to: Aleksander Alekseev (#40)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Tue, Aug 23, 2016 at 4:15 PM, Aleksander Alekseev
<a.alekseev@postgrespro.ru> wrote:

Frankly I have much more faith in Tom's idea of using virtual part of the
catalog for all temporary tables, i.e turning all temporary tables into
"fast" temporary tables. Instead of introducing a new type of temporary tables
that solve catalog bloating problem and forcing users to rewrite applications
why not just not to create a problem in a first place?

Would applications really need to be rewritten? Are they really
constructing temporary tables where the definition of the table is
dynamic, not just the content? I think application authors would be
pretty happy to not need to keep recreating the same tables over and
over again and dealing with DDL in their run-time code. It's not
really rewriting an application to just remove that DDL and move it to
the one-time database schema creation.

I think it's clear we got the idea of temporary tables wrong when we
implemented them and the SQL standard is more useful. It's not just
some implementation artifact that it's possible to implement them in
an efficient way. It's a fundamental design change and experience
shows that separating DDL and making it static while the DML is
dynamic is just a better design.

--
greg

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#49Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Claudio Freire (#47)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/24/2016 12:38 AM, Claudio Freire wrote:

On Tue, Aug 23, 2016 at 7:25 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed to fix
the
catalog bloat problem, as stated in the initial patch submission? We'd
still
need to insert/delete the catalog rows when creating/dropping the
temporary
tables, causing the bloat. Or is there something I'm missing?

Wouldn't more aggressive vacuuming of catalog tables fix the bloat?

Perhaps reserving a worker or N to run only on catalog schemas?

That'd be far simpler.

Maybe, although IIRC the issues with catalog bloat were due to a combination
of long queries and many temporary tables being created/dropped. In that
case simply ramping up autovacuum (or even having a dedicated workers for
catalogs) would not realy help due to the xmin horizon being blocked by the
long-running queries.

Maybe it's entirely crazy idea due to the wine I drank at the dinner, but
couldn't we vacuum the temporary table records differently? For example,
couldn't we just consider them removable as soon as the backend that owns
them disappears?

Or perhaps go all the way and generalize that to rows that never
become visible outside their parent transaction.

As in, delete of rows created by the deleting transaction could clean
up, carefully to avoid voiding indexes and all that, but more
aggressively than regular deletes.

Maybe, but I wouldn't be surprised if such generalization would be an
order of magnitude more complicated - and even the vacuuming changes I
mentioned are undoubtedly a fair amount of work.

Sadly, I don't see how this might fix the other issues mentioned in this
thread (e.g. impossibility to create temp tables on standbys),

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#50Claudio Freire
klaussfreire@gmail.com
In reply to: Greg Stark (#48)
Re: Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Tue, Aug 23, 2016 at 8:50 PM, Greg Stark <stark@mit.edu> wrote:

On Tue, Aug 23, 2016 at 4:15 PM, Aleksander Alekseev
<a.alekseev@postgrespro.ru> wrote:

Frankly I have much more faith in Tom's idea of using virtual part of the
catalog for all temporary tables, i.e turning all temporary tables into
"fast" temporary tables. Instead of introducing a new type of temporary tables
that solve catalog bloating problem and forcing users to rewrite applications
why not just not to create a problem in a first place?

Would applications really need to be rewritten? Are they really
constructing temporary tables where the definition of the table is
dynamic, not just the content?

Mine is. But it wouldn't be a big deal to adapt.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#51Claudio Freire
klaussfreire@gmail.com
In reply to: Tomas Vondra (#49)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Tue, Aug 23, 2016 at 9:12 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

On 08/24/2016 12:38 AM, Claudio Freire wrote:

On Tue, Aug 23, 2016 at 7:25 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed to
fix
the
catalog bloat problem, as stated in the initial patch submission? We'd
still
need to insert/delete the catalog rows when creating/dropping the
temporary
tables, causing the bloat. Or is there something I'm missing?

Wouldn't more aggressive vacuuming of catalog tables fix the bloat?

Perhaps reserving a worker or N to run only on catalog schemas?

That'd be far simpler.

Maybe, although IIRC the issues with catalog bloat were due to a
combination
of long queries and many temporary tables being created/dropped. In that
case simply ramping up autovacuum (or even having a dedicated workers for
catalogs) would not realy help due to the xmin horizon being blocked by
the
long-running queries.

Maybe it's entirely crazy idea due to the wine I drank at the dinner, but
couldn't we vacuum the temporary table records differently? For example,
couldn't we just consider them removable as soon as the backend that owns
them disappears?

Or perhaps go all the way and generalize that to rows that never
become visible outside their parent transaction.

As in, delete of rows created by the deleting transaction could clean
up, carefully to avoid voiding indexes and all that, but more
aggressively than regular deletes.

Maybe, but I wouldn't be surprised if such generalization would be an order
of magnitude more complicated - and even the vacuuming changes I mentioned
are undoubtedly a fair amount of work.

After looking at it from a birdseye view, I agree it's conceptually
complex (reading HeapTupleSatisfiesSelf already makes one dizzy).

But other than that, the implementation seems rather simple. It seems
to me, if one figures out that it is safe to do so (a-priori, xmin not
committed, xmax is current transaction), it would simply be a matter
of chasing the HOT chain root, setting all LP except the first to
LP_UNUSED and the first one to LP_DEAD.

Of course I may be missing a ton of stuff.

Sadly, I don't see how this might fix the other issues mentioned in this
thread (e.g. impossibility to create temp tables on standbys),

No it doesn't :(

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#52Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Claudio Freire (#51)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

Claudio Freire wrote:

After looking at it from a birdseye view, I agree it's conceptually
complex (reading HeapTupleSatisfiesSelf already makes one dizzy).

But other than that, the implementation seems rather simple. It seems
to me, if one figures out that it is safe to do so (a-priori, xmin not
committed, xmax is current transaction), it would simply be a matter
of chasing the HOT chain root, setting all LP except the first to
LP_UNUSED and the first one to LP_DEAD.

Of course I may be missing a ton of stuff.

What you seem to be missing is that rows corresponding to temp tables
are not "visible to its own transaction only". The rows are valid
after the transaction is gone; what makes the tables temporary is the
fact that they are in a temporary schema. And what makes them invisible
to one backend is the fact that they are in the temporary schema for
another backend. Not that they are uncommitted.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#53Claudio Freire
klaussfreire@gmail.com
In reply to: Alvaro Herrera (#52)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Wed, Aug 24, 2016 at 2:04 AM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Claudio Freire wrote:

After looking at it from a birdseye view, I agree it's conceptually
complex (reading HeapTupleSatisfiesSelf already makes one dizzy).

But other than that, the implementation seems rather simple. It seems
to me, if one figures out that it is safe to do so (a-priori, xmin not
committed, xmax is current transaction), it would simply be a matter
of chasing the HOT chain root, setting all LP except the first to
LP_UNUSED and the first one to LP_DEAD.

Of course I may be missing a ton of stuff.

What you seem to be missing is that rows corresponding to temp tables
are not "visible to its own transaction only". The rows are valid
after the transaction is gone; what makes the tables temporary is the
fact that they are in a temporary schema. And what makes them invisible
to one backend is the fact that they are in the temporary schema for
another backend. Not that they are uncommitted.

Yeah, I was thinking of "on commit drop" behavior, but granted there's
all the others.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#54Robert Haas
robertmhaas@gmail.com
In reply to: Tomas Vondra (#41)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Tue, Aug 23, 2016 at 6:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed to fix the
catalog bloat problem, as stated in the initial patch submission? We'd still
need to insert/delete the catalog rows when creating/dropping the temporary
tables, causing the bloat. Or is there something I'm missing?

No, not really. Jim just asked if the idea of partitioning the
columns was completely dead in the water, and I said, no, you could
theoretically salvage it. Whether that does you much good is another
question.

IMV, the point here is that you MUST have globally visible dependency
entries for this to work sanely. If they're not in a catalog, they
have to be someplace else, and backend-private memory isn't good
enough, because that's not globally visible. Until we've got a
strategy for that problem, this whole effort is going nowhere - even
though in other respects it may be a terrific idea.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#55Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Andres Freund (#43)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/24/2016 12:20 AM, Andres Freund wrote:

On 2016-08-23 19:18:04 -0300, Claudio Freire wrote:

On Tue, Aug 23, 2016 at 7:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed to fix the
catalog bloat problem, as stated in the initial patch submission? We'd still
need to insert/delete the catalog rows when creating/dropping the temporary
tables, causing the bloat. Or is there something I'm missing?

Beats me.

Are you puzzled just like me, or are you puzzled why I'm puzzled?

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#56Andres Freund
andres@anarazel.de
In reply to: Tomas Vondra (#55)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On August 24, 2016 9:32:48 AM PDT, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

On 08/24/2016 12:20 AM, Andres Freund wrote:

On 2016-08-23 19:18:04 -0300, Claudio Freire wrote:

On Tue, Aug 23, 2016 at 7:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed

to fix the

catalog bloat problem, as stated in the initial patch submission?

We'd still

need to insert/delete the catalog rows when creating/dropping the

temporary

tables, causing the bloat. Or is there something I'm missing?

Beats me.

Are you puzzled just like me, or are you puzzled why I'm puzzled?

Like you. I don't think this addresses the problem to a significant enough degree to care.

Andres
--
Sent from my Android device with K-9 Mail. Please excuse my brevity.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#57Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#56)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Wed, Aug 24, 2016 at 12:39 PM, Andres Freund <andres@anarazel.de> wrote:

On August 24, 2016 9:32:48 AM PDT, Tomas Vondra <
tomas.vondra@2ndquadrant.com> wrote:

On 08/24/2016 12:20 AM, Andres Freund wrote:

On 2016-08-23 19:18:04 -0300, Claudio Freire wrote:

On Tue, Aug 23, 2016 at 7:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed

to fix the

catalog bloat problem, as stated in the initial patch submission?

We'd still

need to insert/delete the catalog rows when creating/dropping the

temporary

tables, causing the bloat. Or is there something I'm missing?

Beats me.

Are you puzzled just like me, or are you puzzled why I'm puzzled?

Like you. I don't think this addresses the problem to a significant enough
degree to care.

Andres
--
Sent from my Android device with K-9 Mail. Please excuse my brevity.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Ok, here's a wild idea, and it probably depends on having native
partitioning implemented.

Propagate relpersistence, or a boolean flag on (relpersistence = 't') from
pg_class into the child pg_attribute records.

Partition the tables pg_class and pg_attribute first by relpersistence, and
then by oid.

The partitions holding data on persistent objects would basically stay
as-is, but the partition wouldn't have much activity and no temp-table
churn.

The temporary ones, however, would fall into essentially a rotating set of
partitions. Pick enough partitions such that the active transactions only
cover some of the partitions. The rest can be safely truncated by vacuum.

It would mitigate the bloat, existing dictionary queries would still work,
but the additional lookup cost might not be worth it.

#58Vik Fearing
vik@2ndquadrant.fr
In reply to: Robert Haas (#54)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/24/2016 06:16 PM, Robert Haas wrote:

On Tue, Aug 23, 2016 at 6:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed to fix the
catalog bloat problem, as stated in the initial patch submission? We'd still
need to insert/delete the catalog rows when creating/dropping the temporary
tables, causing the bloat. Or is there something I'm missing?

No, not really. Jim just asked if the idea of partitioning the
columns was completely dead in the water, and I said, no, you could
theoretically salvage it. Whether that does you much good is another
question.

IMV, the point here is that you MUST have globally visible dependency
entries for this to work sanely. If they're not in a catalog, they
have to be someplace else, and backend-private memory isn't good
enough, because that's not globally visible. Until we've got a
strategy for that problem, this whole effort is going nowhere - even
though in other respects it may be a terrific idea.

Why not just have a regular-looking table, with a "global temporary"
relpersistence (I don't care which letter it gets) and when a backend
tries to access it, it uses its own private relfilenode instead of
whatever is in pg_class, creating one if necessary. That way the
structure of the table is fixed, with all the dependencies and whatnot,
but the content is private to each backend. What's wrong with this idea?
--
Vik Fearing +33 6 46 75 15 36
http://2ndQuadrant.fr PostgreSQL : Expertise, Formation et Support

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#59Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Vik Fearing (#58)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/31/2016 09:20 PM, Vik Fearing wrote:

On 08/24/2016 06:16 PM, Robert Haas wrote:

On Tue, Aug 23, 2016 at 6:11 PM, Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

Could someone please explain how the unlogged tables are supposed to fix the
catalog bloat problem, as stated in the initial patch submission? We'd still
need to insert/delete the catalog rows when creating/dropping the temporary
tables, causing the bloat. Or is there something I'm missing?

No, not really. Jim just asked if the idea of partitioning the
columns was completely dead in the water, and I said, no, you could
theoretically salvage it. Whether that does you much good is another
question.

IMV, the point here is that you MUST have globally visible dependency
entries for this to work sanely. If they're not in a catalog, they
have to be someplace else, and backend-private memory isn't good
enough, because that's not globally visible. Until we've got a
strategy for that problem, this whole effort is going nowhere - even
though in other respects it may be a terrific idea.

Why not just have a regular-looking table, with a "global temporary"
relpersistence (I don't care which letter it gets) and when a backend
tries to access it, it uses its own private relfilenode instead of
whatever is in pg_class, creating one if necessary. That way the
structure of the table is fixed, with all the dependencies and whatnot,
but the content is private to each backend. What's wrong with this idea?

It's an improvement (and it's pretty much exactly what I proposed
upthread). But it does not solve the problems with pg_statistic for
example (each backend needs it's own statistics. So we'd either bloat
the pg_statistic (if we manage to solve the problem that the table has
the same oid in all backends), or we would need in-memory tuples (just
like discussed in the thread so far).

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#60Andres Freund
andres@anarazel.de
In reply to: Tomas Vondra (#59)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 2016-08-31 23:40:46 +0200, Tomas Vondra wrote:

It's an improvement (and it's pretty much exactly what I proposed
upthread). But it does not solve the problems with pg_statistic for
example (each backend needs it's own statistics. So we'd either bloat
the pg_statistic (if we manage to solve the problem that the table has
the same oid in all backends), or we would need in-memory tuples (just
like discussed in the thread so far).

Creating a session private version of pg_statistic would be fairly
simple.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#61Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Andres Freund (#60)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On 08/31/2016 11:43 PM, Andres Freund wrote:

On 2016-08-31 23:40:46 +0200, Tomas Vondra wrote:

It's an improvement (and it's pretty much exactly what I proposed
upthread). But it does not solve the problems with pg_statistic for
example (each backend needs it's own statistics. So we'd either bloat
the pg_statistic (if we manage to solve the problem that the table has
the same oid in all backends), or we would need in-memory tuples (just
like discussed in the thread so far).

Creating a session private version of pg_statistic would be fairly
simple.

Sure. I'm just saying it's not as simple as overriding relpath.

ISTM we only need the pg_statistics (as other catalogs are connected to
the pg_class entry), which does not have the dependency issues. Or do we
need other catalogs?

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#62Andres Freund
andres@anarazel.de
In reply to: Tomas Vondra (#61)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On August 31, 2016 3:00:15 PM PDT, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

On 08/31/2016 11:43 PM, Andres Freund wrote:

On 2016-08-31 23:40:46 +0200, Tomas Vondra wrote:

It's an improvement (and it's pretty much exactly what I proposed
upthread). But it does not solve the problems with pg_statistic for
example (each backend needs it's own statistics. So we'd either

bloat

the pg_statistic (if we manage to solve the problem that the table

has

the same oid in all backends), or we would need in-memory tuples

(just

like discussed in the thread so far).

Creating a session private version of pg_statistic would be fairly
simple.

Sure. I'm just saying it's not as simple as overriding relpath.

ISTM we only need the pg_statistics (as other catalogs are connected to
the pg_class entry), which does not have the dependency issues. Or do
we
need other catalogs?

In my experience pg attribute is usually the worst affected. Many tech takes won't even have stays entries...

Andres
--
Sent from my Android device with K-9 Mail. Please excuse my brevity.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#63Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#62)
Re: [Patch] Temporary tables that do not bloat pg_catalog (a.k.a fast temp tables)

On Wed, Aug 31, 2016 at 6:07 PM, Andres Freund <andres@anarazel.de> wrote:

In my experience pg attribute is usually the worst affected. Many tech
takes won't even have stays entries...

Mine too. One database currently has a 400GB pg_attribute table, because we
chew through temp tables like popcorn.