diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 3547cdba56e..5049410e81f 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -20,6 +20,8 @@
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
 #include "utils/expandeddatum.h"
+#include "utils/hsearch.h"
+#include "utils/memutils.h"
 #include "utils/rel.h"
 
 static struct varlena *toast_fetch_datum(struct varlena *attr);
@@ -29,6 +31,30 @@ static struct varlena *toast_fetch_datum_slice(struct varlena *attr,
 static struct varlena *toast_decompress_datum(struct varlena *attr);
 static struct varlena *toast_decompress_datum_slice(struct varlena *attr, int32 slicelength);
 
+bool enable_toast_cache = false;
+
+HTAB *toastCache = NULL;
+MemoryContext ToastCacheContext = NULL;
+
+static uint64	toast_cache_hits = 0;
+static uint64	toast_cache_misses = 0;
+
+typedef struct toast_cache_key {
+	Oid toastrelid;
+	Oid valueid;
+} toast_cache_key;
+
+typedef struct toast_cache_entry {
+	toast_cache_key key;
+	int32	size;
+	void   *data;
+} toast_cache_entry;
+
+static toast_cache_entry *toast_cache_lookup(Oid toastrelid, Oid valueid);
+static void toast_cache_add_entry(Oid toastrelid, Oid valueid, struct varlena *attr);
+static struct varlena *toast_cache_lookup_datum(toast_cache_key key);
+static void toast_cache_store_datum(toast_cache_key key, struct varlena *attr);
+
 /* ----------
  * detoast_external_attr -
  *
@@ -117,6 +143,28 @@ detoast_attr(struct varlena *attr)
 {
 	if (VARATT_IS_EXTERNAL_ONDISK(attr))
 	{
+		struct varlena *result;
+		toast_cache_key	key;
+
+		/* calculate the cache key */
+		{
+			struct varatt_external toast_pointer;
+
+			/* Must copy to access aligned fields */
+			VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
+
+			/* try lookup in the toast cache */
+			key.toastrelid = toast_pointer.va_toastrelid;
+			key.valueid = toast_pointer.va_valueid;
+		}
+
+		/* first try to lookup in the toast cache */
+		result = toast_cache_lookup_datum(key);
+
+		/* cache hit, we're done */
+		if (result)
+			return result;
+
 		/*
 		 * This is an externally stored datum --- fetch it back from there
 		 */
@@ -129,6 +177,8 @@ detoast_attr(struct varlena *attr)
 			attr = toast_decompress_datum(tmp);
 			pfree(tmp);
 		}
+
+		toast_cache_store_datum(key, attr);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -644,3 +694,130 @@ toast_datum_size(Datum value)
 	}
 	return result;
 }
+
+static bool
+toast_cache_init(void)
+{
+	HASHCTL		ctl;
+
+	if (!enable_toast_cache)
+		return false;
+
+	/* already initialized */
+	if (toastCache)
+		return true;
+
+	toast_cache_hits = 0;
+	toast_cache_misses = 0;
+
+	/* FIXME should really be per query */
+	ToastCacheContext = AllocSetContextCreate(TopMemoryContext,
+											  "TOAST cache context",
+											  ALLOCSET_DEFAULT_SIZES);
+
+	/* Make a new hash table for the cache */
+	ctl.keysize = sizeof(toast_cache_key);
+	ctl.entrysize = sizeof(toast_cache_entry);
+	ctl.hcxt = ToastCacheContext;
+
+	toastCache = hash_create("TOAST cache",
+							 128, &ctl,
+							 HASH_ELEM | HASH_CONTEXT | HASH_BLOBS);
+
+	return true;
+}
+
+static toast_cache_entry *
+toast_cache_lookup(Oid toastrelid, Oid valueid)
+{
+	toast_cache_key		key;
+	toast_cache_entry  *entry;
+
+	/* make sure cache initialized */
+	if (!toast_cache_init())
+		return NULL;
+
+	key.toastrelid = toastrelid;
+	key.valueid = valueid;
+
+	entry = (toast_cache_entry *) hash_search(toastCache, &key,
+											  HASH_FIND, NULL);
+
+	if (entry)
+		toast_cache_hits++;
+	else
+		toast_cache_misses++;
+
+	return entry;
+}
+
+static void
+toast_cache_add_entry(Oid toastrelid, Oid valueid, struct varlena *attr)
+{
+	bool				found = false;
+	toast_cache_key		key;
+	toast_cache_entry  *entry;
+
+	/* make sure cache initialized */
+	if (!toast_cache_init())
+		return;
+
+	key.toastrelid = toastrelid;
+	key.valueid = valueid;
+
+	entry = (toast_cache_entry *) hash_search(toastCache, &key,
+											  HASH_ENTER, &found);
+
+	Assert(!found);
+
+	entry->size = VARSIZE_ANY_EXHDR(attr);
+	entry->data = MemoryContextAlloc(ToastCacheContext, entry->size);
+	memcpy(entry->data, VARDATA_ANY(attr), entry->size);
+}
+
+static void
+toast_cache_destroy(void)
+{
+	if (!toastCache)
+		return;
+
+	elog(LOG, "AtEOXact_ToastCache hits %ld misses %ld ratio %.2f%%",
+		 toast_cache_hits, toast_cache_misses,
+		 (toast_cache_hits * 100.0) / Max(1, toast_cache_hits + toast_cache_misses));
+
+	MemoryContextDelete(ToastCacheContext);
+
+	toastCache = NULL;
+	ToastCacheContext = NULL;
+}
+
+void
+AtEOXact_ToastCache(void)
+{
+	toast_cache_destroy();
+}
+
+static struct varlena *
+toast_cache_lookup_datum(toast_cache_key key)
+{
+	toast_cache_entry *entry;
+	struct varlena *result = NULL;
+
+	entry = toast_cache_lookup(key.toastrelid, key.valueid);
+
+	if (!entry)
+		return NULL;
+
+	result = (struct varlena *) palloc(entry->size + VARHDRSZ);
+	SET_VARSIZE(result, entry->size + VARHDRSZ);
+
+	memcpy(VARDATA(result), entry->data, entry->size);
+
+	return result;
+}
+
+static void
+toast_cache_store_datum(toast_cache_key key, struct varlena *attr)
+{
+	toast_cache_add_entry(key.toastrelid, key.valueid, attr);
+}
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 70ab6e27a13..2cae3582026 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -21,6 +21,7 @@
 #include <unistd.h>
 
 #include "access/commit_ts.h"
+#include "access/detoast.h"
 #include "access/multixact.h"
 #include "access/parallel.h"
 #include "access/subtrans.h"
@@ -2381,6 +2382,7 @@ CommitTransaction(void)
 	AtEOXact_Snapshot(true, false);
 	AtEOXact_ApplyLauncher(true);
 	AtEOXact_LogicalRepWorkers(true);
+	AtEOXact_ToastCache();
 	pgstat_report_xact_timestamp(0);
 
 	ResourceOwnerDelete(TopTransactionResourceOwner);
@@ -2671,6 +2673,7 @@ PrepareTransaction(void)
 	/* we treat PREPARE as ROLLBACK so far as waking workers goes */
 	AtEOXact_ApplyLauncher(false);
 	AtEOXact_LogicalRepWorkers(false);
+	AtEOXact_ToastCache();
 	pgstat_report_xact_timestamp(0);
 
 	CurrentResourceOwner = NULL;
@@ -2888,6 +2891,7 @@ AbortTransaction(void)
 		AtEOXact_PgStat(false, is_parallel_worker);
 		AtEOXact_ApplyLauncher(false);
 		AtEOXact_LogicalRepWorkers(false);
+		AtEOXact_ToastCache();
 		pgstat_report_xact_timestamp(0);
 	}
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 93ded31ed92..ceb3d3b953c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -27,6 +27,7 @@
 #endif
 
 #include "access/commit_ts.h"
+#include "access/detoast.h"
 #include "access/gin.h"
 #include "access/slru.h"
 #include "access/toast_compression.h"
@@ -1015,6 +1016,16 @@ struct config_bool ConfigureNamesBool[] =
 		true,
 		NULL, NULL, NULL
 	},
+	{
+		{"enable_toast_cache", PGC_USERSET, QUERY_TUNING_METHOD,
+			gettext_noop("Enables caching of detoasted values."),
+			NULL,
+			GUC_EXPLAIN
+		},
+		&enable_toast_cache,
+		false,
+		NULL, NULL, NULL
+	},
 	{
 		{"geqo", PGC_USERSET, QUERY_TUNING_GEQO,
 			gettext_noop("Enables genetic query optimization."),
diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 12d8cdb356a..1a662c81388 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -79,4 +79,7 @@ extern Size toast_raw_datum_size(Datum value);
  */
 extern Size toast_datum_size(Datum value);
 
+extern PGDLLIMPORT bool enable_toast_cache;
+extern void AtEOXact_ToastCache(void);
+
 #endif							/* DETOAST_H */
