agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH 2/3] Remove entries that haven't been used for a certain time 16+ messages / 3 participants [nested] [flat]
* [PATCH 3/5] Remove entries that haven't been used for a certain time @ 2018-10-16 04:04 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw) Catcache entries can be left alone for several reasons. It is not desirable that they eat up memory. With this patch, This adds consideration of removal of entries that haven't been used for a certain time before enlarging the hash array. This also can put a hard limit on the number of catcache entries. --- doc/src/sgml/config.sgml | 40 ++++ src/backend/tcop/postgres.c | 13 ++ src/backend/utils/cache/catcache.c | 283 +++++++++++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 11 + src/backend/utils/misc/guc.c | 43 ++++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/miscadmin.h | 1 + src/include/utils/catcache.h | 50 ++++- src/include/utils/timeout.h | 1 + 10 files changed, 436 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 07b847a8e9..4749ad61a9 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1661,6 +1661,46 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + system catalog cache entry is removed. -1 indicates that this feature + is disabled at all. The value defaults to 300 seconds (<literal>5 + minutes</literal>). The catalog cache entries that are not used for + the duration can be removed to prevent it from being filled up with + useless entries. This behaviour is muted until the size of a catalog + cache exceeds <xref linkend="guc-catalog-cache-memory-target"/>. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-catalog-cache-memory-target" xreflabel="catalog_cache_memory_target"> + <term><varname>catalog_cache_memory_target</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_memory_target</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum amount of memory to which a system catalog cache + can expand without pruning in kilobytes. The value defaults to 0, + indicating that age-based pruning is always considered. After + exceeding this size, catalog cache starts pruning according to + <xref linkend="guc-catalog-cache-prune-min-age"/>. If you need to keep + certain amount of catalog cache entries with intermittent usage, try + increase this setting. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 36cfd507b2..f192ee2ca6 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2584,6 +2585,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void @@ -3159,6 +3161,14 @@ ProcessInterrupts(void) if (ParallelMessagePending) HandleParallelMessages(); + + if (CatcacheClockTimeoutPending) + { + CatcacheClockTimeoutPending = 0; + + /* Update timetamp then set up the next timeout */ + UpdateCatCacheClock(); + } } @@ -4021,6 +4031,9 @@ PostgresMain(int argc, char *argv[], QueryCancelPending = false; /* second to avoid race condition */ stmt_timeout_active = false; + /* get sync with the timer state */ + catcache_clock_timeout_active = false; + /* Not reading from the client anymore. */ DoingCommandRead = false; diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 258a1d64cc..04a60a490a 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -39,6 +39,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -71,9 +72,43 @@ #define CACHE6_elog(a,b,c,d,e,f,g) #endif +/* GUC variable to define the minimum age of entries that will be considered to + * be evicted in seconds. This variable is shared among various cache + * mechanisms. + */ +int catalog_cache_prune_min_age = 300; + +/* + * GUC variable to define the minimum size of hash to cosider entry eviction. + * This variable is shared among various cache mechanisms. + */ +int catalog_cache_memory_target = 0; + +/* + * GUC for limit by the number of entries. Entries are removed when the number + * of them goes above catalog_cache_entry_limit and leaving newer entries by + * the ratio specified by catalog_cache_prune_ratio. + */ +int catalog_cache_entry_limit = 0; +double catalog_cache_prune_ratio = 0.8; + +/* + * Flag to keep track of whether catcache clock timer is active. + */ +bool catcache_clock_timeout_active = false; + +/* + * Minimum interval between two success move of a cache entry in LRU list, + * in microseconds. + */ +#define MIN_LRU_UPDATE_INTERVAL 100000 /* 100ms */ + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock used to record the last accessed time of a catcache record. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -481,6 +516,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) /* delink from linked list */ dlist_delete(&ct->cache_elem); + dlist_delete(&ct->lru_node); /* * Free keys when we're dealing with a negative entry, normal entries just @@ -490,6 +526,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, ct->keys); + cache->cc_memusage -= ct->size; pfree(ct); --cache->cc_ntup; @@ -779,6 +816,7 @@ InitCatCache(int id, MemoryContext oldcxt; size_t sz; int i; + uint64 base_size; /* * nbuckets is the initial number of hash buckets to use in this catcache. @@ -821,8 +859,12 @@ InitCatCache(int id, * * Note: we rely on zeroing to initialize all the dlist headers correctly */ + base_size = MemoryContextGetConsumption(CacheMemoryContext); sz = sizeof(CatCache) + PG_CACHE_LINE_SIZE; cp = (CatCache *) CACHELINEALIGN(palloc0(sz)); + cp->cc_head_alloc_size = + MemoryContextGetConsumption(CacheMemoryContext) - base_size; + cp->cc_bucket = palloc0(nbuckets * sizeof(dlist_head)); /* @@ -842,6 +884,11 @@ InitCatCache(int id, for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + /* cc_head_alloc_size + consumed size for cc_bucket */ + cp->cc_memusage = + MemoryContextGetConsumption(CacheMemoryContext) - base_size; + + dlist_init(&cp->cc_lru_list); /* * new cache is initialized as far as we can go for now. print some * debugging information, if appropriate. @@ -858,9 +905,185 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * helper routine for SetCatCacheClock and UpdateCatCacheClockTimer. + * + * We need to maintain the catcache clock during a long query. + */ +void +SetupCatCacheClockTimer(void) +{ + long delay; + + /* stop timer if not needed */ + if (catalog_cache_prune_min_age == 0) + { + catcache_clock_timeout_active = false; + return; + } + + /* One 10th of the variable, in milliseconds */ + delay = catalog_cache_prune_min_age * 1000/10; + + /* Lower limit is 1 second */ + if (delay < 1000) + delay = 1000; + + enable_timeout_after(CATCACHE_CLOCK_TIMEOUT, delay); + + catcache_clock_timeout_active = true; +} + +/* + * Update catcacheclock: this is intended to be called from + * CATCACHE_CLOCK_TIMEOUT. The interval is expected more than 1 second (see + * above), so GetCurrentTime() doesn't harm. + */ +void +UpdateCatCacheClock(void) +{ + catcacheclock = GetCurrentTimestamp(); + SetupCatCacheClockTimer(); +} + +/* + * It may take an unexpectedly long time before the next clock update when + * catalog_cache_prune_min_age gets shorter. Disabling the current timer let + * the next update happen at the expected interval. We don't necessariry + * require this for increase the age but we don't need to avoid to disable + * either. + */ +void +assign_catalog_cache_prune_min_age(int newval, void *extra) +{ + if (catcache_clock_timeout_active) + disable_timeout(CATCACHE_CLOCK_TIMEOUT, false); + + catcache_clock_timeout_active = false; +} + +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries can be left alone for several reasons. We remove them if + * they are not accessed for a certain time to prevent catcache from + * bloating. The eviction is performed with the similar algorithm with buffer + * eviction using access counter. Entries that are accessed several times can + * live longer than those that have had less access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + int nelems_before = cp->cc_ntup; + int ndelelems = 0; + bool prune_by_age = false; + bool prune_by_number = false; + dlist_mutable_iter iter; + + /* prune only if the size of the hash is above the target */ + if (catalog_cache_prune_min_age >= 0 && + cp->cc_memusage > (Size) catalog_cache_memory_target * 1024L) + prune_by_age = true; + + if (catalog_cache_entry_limit > 0 && + nelems_before >= catalog_cache_entry_limit) + { + ndelelems = nelems_before - + (int) (catalog_cache_entry_limit * catalog_cache_prune_ratio); + + /* an arbitrary lower limit.. */ + if (ndelelems < 256) + ndelelems = 256; + if (ndelelems > nelems_before) + ndelelems = nelems_before; + + prune_by_number = true; + } + + /* Return immediately if no pruning is wanted */ + if (!prune_by_age && !prune_by_number) + return false; + + /* Scan over LRU to find entries to remove */ + dlist_foreach_modify(iter, &cp->cc_lru_list) + { + CatCTup *ct = dlist_container(CatCTup, lru_node, iter.cur); + bool remove_this = false; + + /* We don't remove referenced entry */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* check against age */ + if (prune_by_age) + { + long entry_age; + int us; + + /* + * Calculate the duration from the time of the last access to the + * "current" time. Since catcacheclock is not advanced within a + * transaction, the entries that are accessed within the current + * transaction won't be pruned. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < catalog_cache_prune_min_age) + { + /* no longer have a business with further entries, exit */ + prune_by_age = false; + break; + } + /* + * Entries that are not accessed after last pruning are removed in + * that seconds, and that has been accessed several times are + * removed after leaving alone for up to three times of the + * duration. We don't try shrink buckets since pruning effectively + * caps catcache expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + remove_this = true; + } + + /* check against entry number */ + if (prune_by_number) + { + if (nremoved < ndelelems) + remove_this = true; + else + prune_by_number = false; /* we're satisfied */ + } + + /* exit immediately if all finished */ + if (!prune_by_age && !prune_by_number) + break; + + /* do the work */ + if (remove_this) + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, nelems_before); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -870,6 +1093,7 @@ RehashCatCache(CatCache *cp) dlist_head *newbucket; int newnbuckets; int i; + uint64 base_size = MemoryContextGetConsumption(CacheMemoryContext); elog(DEBUG1, "rehashing catalog cache id %d for %s; %d tups, %d buckets", cp->id, cp->cc_relname, cp->cc_ntup, cp->cc_nbuckets); @@ -878,6 +1102,10 @@ RehashCatCache(CatCache *cp) newnbuckets = cp->cc_nbuckets * 2; newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, newnbuckets * sizeof(dlist_head)); + /* recalculate memory usage from the first */ + cp->cc_memusage = cp->cc_head_alloc_size + + MemoryContextGetConsumption(CacheMemoryContext) - base_size; + /* Move all entries from old hash table to new. */ for (i = 0; i < cp->cc_nbuckets; i++) { @@ -890,6 +1118,7 @@ RehashCatCache(CatCache *cp) dlist_delete(iter.cur); dlist_push_head(&newbucket[hashIndex], &ct->cache_elem); + cp->cc_memusage += ct->size; } } @@ -1274,6 +1503,21 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* Update access information for pruning */ + if (ct->naccess < 2) + ct->naccess++; + + /* + * We don't want too frequent update of + * LRU. catalog_cache_prune_min_age can be changed on-session so we + * need to maintain the LRU regardless of catalog_cache_prune_min_age. + */ + if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL) + { + ct->lastaccess = catcacheclock; + dlist_move_tail(&cache->cc_lru_list, &ct->lru_node); + } + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1709,6 +1953,11 @@ SearchCatCacheList(CatCache *cache, /* Now we can build the CatCList entry. */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); nmembers = list_length(ctlist); + + /* + * Don't waste a time by counting the list in catcache memory usage, + * since it doesn't live a long life. + */ cl = (CatCList *) palloc(offsetof(CatCList, members) + nmembers * sizeof(CatCTup *)); @@ -1819,11 +2068,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, CatCTup *ct; HeapTuple dtp; MemoryContext oldcxt; + uint64 base_size = MemoryContextGetConsumption(CacheMemoryContext); /* negative entries have no tuple associated */ if (ntp) { int i; + int tupsize; Assert(!negative); @@ -1842,8 +2093,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, /* Allocate memory for CatCTup and the cached tuple in one go */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + tupsize = sizeof(CatCTup) + MAXIMUM_ALIGNOF + dtp->t_len; + ct = (CatCTup *) palloc(tupsize); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; @@ -1877,7 +2128,6 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, Assert(negative); oldcxt = MemoryContextSwitchTo(CacheMemoryContext); ct = (CatCTup *) palloc(sizeof(CatCTup)); - /* * Store keys - they'll point into separately allocated memory if not * by-value. @@ -1898,18 +2148,36 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + dlist_push_tail(&cache->cc_lru_list, &ct->lru_node); dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + ct->size = MemoryContextGetConsumption(CacheMemoryContext) - base_size; + cache->cc_memusage += ct->size; + + /* increase refcount so that this survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try cleanup by removing + * infrequently used entries to make a room for the new entry. If it + * failed, enlarge the bucket array instead. Quite arbitrarily, we try + * this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + /* we may still want to prune by entry number, check it */ + else if (catalog_cache_entry_limit > 0 && + cache->cc_ntup > catalog_cache_entry_limit) + CatCacheCleanupOldEntries(cache); + + ct->refcount--; return ct; } @@ -1940,7 +2208,7 @@ CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *keys) /* * Helper routine that copies the keys in the srckeys array into the dstkeys * one, guaranteeing that the datums are fully allocated in the current memory - * context. + * context. Returns allocated memory size. */ static void CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, @@ -1976,7 +2244,6 @@ CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, att->attbyval, att->attlen); } - } /* diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index fd51934aaf..0e8b972a29 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -32,6 +32,7 @@ volatile sig_atomic_t QueryCancelPending = false; volatile sig_atomic_t ProcDiePending = false; volatile sig_atomic_t ClientConnectionLost = false; volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false; +volatile sig_atomic_t CatcacheClockTimeoutPending = false; volatile sig_atomic_t ConfigReloadPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index a5ee209f91..9eb50e9676 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -72,6 +72,7 @@ static void ShutdownPostgres(int code, Datum arg); static void StatementTimeoutHandler(void); static void LockTimeoutHandler(void); static void IdleInTransactionSessionTimeoutHandler(void); +static void CatcacheClockTimeoutHandler(void); static bool ThereIsAtLeastOneRole(void); static void process_startup_options(Port *port, bool am_superuser); static void process_settings(Oid databaseid, Oid roleid); @@ -628,6 +629,8 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username, RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler); RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT, IdleInTransactionSessionTimeoutHandler); + RegisterTimeout(CATCACHE_CLOCK_TIMEOUT, + CatcacheClockTimeoutHandler); } /* @@ -1238,6 +1241,14 @@ IdleInTransactionSessionTimeoutHandler(void) SetLatch(MyLatch); } +static void +CatcacheClockTimeoutHandler(void) +{ + CatcacheClockTimeoutPending = true; + InterruptPending = true; + SetLatch(MyLatch); +} + /* * Returns true if at least one role is defined in this database cluster. */ diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 41d477165c..c62d5ad8b8 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2205,6 +2206,38 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum unused duration of cache entries before removal."), + gettext_noop("Catalog cache entries that live unused for longer than this seconds are considered to be removed."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, assign_catalog_cache_prune_min_age, NULL + }, + + { + {"catalog_cache_memory_target", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum syscache size to keep."), + gettext_noop("Time-based cache pruning starts working after exceeding this size."), + GUC_UNIT_KB + }, + &catalog_cache_memory_target, + 0, 0, MAX_KILOBYTES, + NULL, NULL, NULL + }, + + { + {"catalog_cache_entry_limit", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &catalog_cache_entry_limit, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if @@ -3368,6 +3401,16 @@ static struct config_real ConfigureNamesReal[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_ratio", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Reduce ratio of pruning caused by catalog_cache_entry_limit."), + NULL + }, + &catalog_cache_prune_ratio, + 0.8, 0.0, 1.0, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index ad6c436f93..aeb5968e75 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,8 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_memory_target = 0kB # in kB +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index c9e35003a5..33b800e80f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -82,6 +82,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending; extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending; extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending; extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t CatcacheClockTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ConfigReloadPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..0a714bf514 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,11 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + dlist_head cc_lru_list; + int cc_head_alloc_size;/* consumed memory to allocate this struct */ + int cc_memusage; /* memory usage of this catcache (excluding + * header part) */ + int cc_nfreeent; /* # of entries currently not referenced */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,7 +125,10 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ - + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* approx. timestamp of the last usage */ + dlist_node lru_node; /* LRU node */ + int size; /* palloc'ed size off this tuple */ /* * The tuple may also be a member of at most one CatCList. (If a single * catcache is list-searched with varying numbers of keys, we may have to @@ -189,6 +198,45 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; +extern int catalog_cache_memory_target; +extern int catalog_cache_entry_limit; +extern double catalog_cache_prune_ratio; + +/* to use as access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* + * Flag to keep track of whether catcache timestamp timer is active. + */ +extern bool catcache_clock_timeout_active; + +/* catcache prune time helper functions */ +extern void SetupCatCacheClockTimer(void); +extern void UpdateCatCacheClock(void); + +/* + * SetCatCacheClock - set timestamp for catcache access record and start + * maintenance timer if needed. We keep to update the clock even while pruning + * is disable so that we are not confused by bogus clock value. + */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; + + if (!catcache_clock_timeout_active && catalog_cache_prune_min_age > 0) + SetupCatCacheClockTimer(); +} + +static inline TimestampTz +GetCatCacheClock(void) +{ + return catcacheclock; +} + +extern void assign_catalog_cache_prune_min_age(int newval, void *extra); extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h index 9244a2a7b7..b2d97b4f7b 100644 --- a/src/include/utils/timeout.h +++ b/src/include/utils/timeout.h @@ -31,6 +31,7 @@ typedef enum TimeoutId STANDBY_TIMEOUT, STANDBY_LOCK_TIMEOUT, IDLE_IN_TRANSACTION_SESSION_TIMEOUT, + CATCACHE_CLOCK_TIMEOUT, /* First user-definable timeout reason */ USER_TIMEOUT, /* Maximum number of timeout reasons */ -- 2.16.3 ----Next_Part(Wed_Feb_13_15_31_14_2019_467)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v15-0004-Syscache-usage-tracking-feature.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 2/2] Remove entries that haven't been used for a certain time @ 2018-10-16 04:04 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw) Catcache entries can be left alone for several reasons. It is not desirable that they eat up memory. With this patch, This adds consideration of removal of entries that haven't been used for a certain time before enlarging the hash array. This also can put a hard limit on the number of catcache entries. --- doc/src/sgml/config.sgml | 40 +++++ src/backend/tcop/postgres.c | 13 ++ src/backend/utils/cache/catcache.c | 243 ++++++++++++++++++++++++-- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 11 ++ src/backend/utils/misc/guc.c | 23 +++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/miscadmin.h | 1 + src/include/utils/catcache.h | 43 ++++- src/include/utils/timeout.h | 1 + 10 files changed, 364 insertions(+), 14 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 8bd57f376b..7a93aef659 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1661,6 +1661,46 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + system catalog cache entry is removed. -1 indicates that this feature + is disabled at all. The value defaults to 300 seconds (<literal>5 + minutes</literal>). The catalog cache entries that are not used for + the duration can be removed to prevent it from being filled up with + useless entries. This behaviour is muted until the size of a catalog + cache exceeds <xref linkend="guc-catalog-cache-memory-target"/>. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-catalog-cache-memory-target" xreflabel="catalog_cache_memory_target"> + <term><varname>catalog_cache_memory_target</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_memory_target</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum amount of memory to which a system catalog cache + can expand without pruning in kilobytes. The value defaults to 0, + indicating that age-based pruning is always considered. After + exceeding this size, catalog cache starts pruning according to + <xref linkend="guc-catalog-cache-prune-min-age"/>. If you need to keep + certain amount of catalog cache entries with intermittent usage, try + increase this setting. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 8b4d94c9a1..d9a54ed37f 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2584,6 +2585,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void @@ -3159,6 +3161,14 @@ ProcessInterrupts(void) if (ParallelMessagePending) HandleParallelMessages(); + + if (CatcacheClockTimeoutPending) + { + CatcacheClockTimeoutPending = false; + + /* Update timestamp then set up the next timeout */ + UpdateCatCacheClock(); + } } @@ -4021,6 +4031,9 @@ PostgresMain(int argc, char *argv[], QueryCancelPending = false; /* second to avoid race condition */ stmt_timeout_active = false; + /* get sync with the timer state */ + catcache_clock_timeout_active = false; + /* Not reading from the client anymore. */ DoingCommandRead = false; diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 78dd5714fa..30ab710aaa 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -39,6 +39,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -61,9 +62,35 @@ #define CACHE_elog(...) #endif +/* GUC variable to define the minimum age of entries that will be considered to + * be evicted in seconds. This variable is shared among various cache + * mechanisms. + */ +int catalog_cache_prune_min_age = 300; + +/* + * GUC variable to define the minimum size of hash to cosider entry eviction. + * This variable is shared among various cache mechanisms. + */ +int catalog_cache_memory_target = 0; + +/* + * Flag to keep track of whether catcache clock timer is active. + */ +bool catcache_clock_timeout_active = false; + +/* + * Minimum interval between two success move of a cache entry in LRU list, + * in microseconds. + */ +#define MIN_LRU_UPDATE_INTERVAL 100000 /* 100ms */ + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock used to record the last accessed time of a catcache record. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -97,7 +124,7 @@ static CatCTup *CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, static void CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *keys); -static void CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, +static size_t CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *srckeys, Datum *dstkeys); @@ -469,6 +496,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) /* delink from linked list */ dlist_delete(&ct->cache_elem); + dlist_delete(&ct->lru_node); /* * Free keys when we're dealing with a negative entry, normal entries just @@ -478,6 +506,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, ct->keys); + cache->cc_memusage -= ct->size; pfree(ct); --cache->cc_ntup; @@ -811,7 +840,9 @@ InitCatCache(int id, */ sz = sizeof(CatCache) + PG_CACHE_LINE_SIZE; cp = (CatCache *) CACHELINEALIGN(palloc0(sz)); - cp->cc_bucket = palloc0(nbuckets * sizeof(dlist_head)); + cp->cc_head_size = sz; + sz = nbuckets * sizeof(dlist_head); + cp->cc_bucket = palloc0(sz); /* * initialize the cache's relation information for the relation @@ -830,6 +861,9 @@ InitCatCache(int id, for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + cp->cc_memusage = cp->cc_head_size + sz; + + dlist_init(&cp->cc_lru_list); /* * new cache is initialized as far as we can go for now. print some * debugging information, if appropriate. @@ -846,9 +880,143 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * helper routine for SetCatCacheClock and UpdateCatCacheClockTimer. + * + * We need to maintain the catcache clock during a long query. + */ +void +SetupCatCacheClockTimer(void) +{ + long delay; + + /* stop timer if not needed */ + if (catalog_cache_prune_min_age == 0) + { + catcache_clock_timeout_active = false; + return; + } + + /* One 10th of the variable, in milliseconds */ + delay = catalog_cache_prune_min_age * 1000/10; + + /* Lower limit is 1 second */ + if (delay < 1000) + delay = 1000; + + enable_timeout_after(CATCACHE_CLOCK_TIMEOUT, delay); + + catcache_clock_timeout_active = true; +} + +/* + * Update catcacheclock: this is intended to be called from + * CATCACHE_CLOCK_TIMEOUT. The interval is expected more than 1 second (see + * above), so GetCurrentTime() doesn't harm. + */ +void +UpdateCatCacheClock(void) +{ + catcacheclock = GetCurrentTimestamp(); + SetupCatCacheClockTimer(); +} + +/* + * It may take an unexpectedly long time before the next clock update when + * catalog_cache_prune_min_age gets shorter. Disabling the current timer let + * the next update happen at the expected interval. We don't necessariry + * require this for increase the age but we don't need to avoid to disable + * either. + */ +void +assign_catalog_cache_prune_min_age(int newval, void *extra) +{ + if (catcache_clock_timeout_active) + disable_timeout(CATCACHE_CLOCK_TIMEOUT, false); + + catcache_clock_timeout_active = false; +} + +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries can be left alone for several reasons. We remove them if + * they are not accessed for a certain time to prevent catcache from + * bloating. The eviction is performed with the similar algorithm with buffer + * eviction using access counter. Entries that are accessed several times can + * live longer than those that have had less access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + dlist_mutable_iter iter; + + /* Return immediately if no pruning is wanted */ + if (catalog_cache_prune_min_age == 0 || + cp->cc_memusage <= (Size) catalog_cache_memory_target * 1024L) + return false; + + /* Scan over LRU to find entries to remove */ + dlist_foreach_modify(iter, &cp->cc_lru_list) + { + CatCTup *ct = dlist_container(CatCTup, lru_node, iter.cur); + long entry_age; + int us; + + /* We don't remove referenced entry */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* + * Calculate the duration from the time from the last access to + * the "current" time. catcacheclock is updated per-statement + * basis and additionaly udpated periodically during a long + * running query. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < catalog_cache_prune_min_age) + { + /* + * no longer have a business with further entries, exit. At least + * one removal is enough to prevent rehashing this time. + */ + return nremoved > 0; + } + + /* + * Entries that are not accessed after last pruning are removed in + * that seconds, and that has been accessed several times are + * removed after leaving alone for up to three times of the + * duration. We don't try shrink buckets since pruning effectively + * caps catcache expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + { + /* remove this entry */ + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -858,13 +1026,18 @@ RehashCatCache(CatCache *cp) dlist_head *newbucket; int newnbuckets; int i; + size_t sz; elog(DEBUG1, "rehashing catalog cache id %d for %s; %d tups, %d buckets", cp->id, cp->cc_relname, cp->cc_ntup, cp->cc_nbuckets); /* Allocate a new, larger, hash table. */ newnbuckets = cp->cc_nbuckets * 2; - newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, newnbuckets * sizeof(dlist_head)); + sz = newnbuckets * sizeof(dlist_head); + newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, sz); + + /* reset memory usage */ + cp->cc_memusage = cp->cc_head_size + sz; /* Move all entries from old hash table to new. */ for (i = 0; i < cp->cc_nbuckets; i++) @@ -878,6 +1051,7 @@ RehashCatCache(CatCache *cp) dlist_delete(iter.cur); dlist_push_head(&newbucket[hashIndex], &ct->cache_elem); + cp->cc_memusage += ct->size; } } @@ -1260,6 +1434,21 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* Update access information for pruning */ + if (ct->naccess < 2) + ct->naccess++; + + /* + * We don't want too frequent update of + * LRU. catalog_cache_prune_min_age can be changed on-session so we + * need to maintain the LRU regardless of catalog_cache_prune_min_age. + */ + if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL) + { + ct->lastaccess = catcacheclock; + dlist_move_tail(&cache->cc_lru_list, &ct->lru_node); + } + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1695,6 +1884,11 @@ SearchCatCacheList(CatCache *cache, /* Now we can build the CatCList entry. */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); nmembers = list_length(ctlist); + + /* + * Don't waste a time by counting the list in catcache memory usage, + * since it doesn't live a long life. + */ cl = (CatCList *) palloc(offsetof(CatCList, members) + nmembers * sizeof(CatCTup *)); @@ -1805,6 +1999,7 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, CatCTup *ct; HeapTuple dtp; MemoryContext oldcxt; + int tupsize; /* negative entries have no tuple associated */ if (ntp) @@ -1828,8 +2023,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, /* Allocate memory for CatCTup and the cached tuple in one go */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + tupsize = sizeof(CatCTup) + MAXIMUM_ALIGNOF + dtp->t_len; + ct = (CatCTup *) palloc(tupsize); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; @@ -1862,14 +2057,16 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, { Assert(negative); oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup)); + tupsize = sizeof(CatCTup); + ct = (CatCTup *) palloc(tupsize); /* * Store keys - they'll point into separately allocated memory if not * by-value. */ - CatCacheCopyKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, - arguments, ct->keys); + tupsize += + CatCacheCopyKeys(cache->cc_tupdesc, cache->cc_nkeys, + cache->cc_keyno, arguments, ct->keys); MemoryContextSwitchTo(oldcxt); } @@ -1884,19 +2081,33 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + dlist_push_tail(&cache->cc_lru_list, &ct->lru_node); dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + ct->size = tupsize; + cache->cc_memusage += ct->size; + + /* increase refcount so that this survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try cleanup by removing + * infrequently used entries to make a room for the new entry. If it + * failed, enlarge the bucket array instead. Quite arbitrarily, we try + * this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + ct->refcount--; + return ct; } @@ -1926,13 +2137,14 @@ CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *keys) /* * Helper routine that copies the keys in the srckeys array into the dstkeys * one, guaranteeing that the datums are fully allocated in the current memory - * context. + * context. Returns allocated memory size. */ -static void +static size_t CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *srckeys, Datum *dstkeys) { int i; + size_t sz = 0; /* * XXX: memory and lookup performance could possibly be improved by @@ -1961,8 +2173,13 @@ CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, dstkeys[i] = datumCopy(src, att->attbyval, att->attlen); + + /* approximate size */ + if (!att->attbyval) + sz += VARHDRSZ + att->attlen; } + return sz; } /* diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index fd51934aaf..0e8b972a29 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -32,6 +32,7 @@ volatile sig_atomic_t QueryCancelPending = false; volatile sig_atomic_t ProcDiePending = false; volatile sig_atomic_t ClientConnectionLost = false; volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false; +volatile sig_atomic_t CatcacheClockTimeoutPending = false; volatile sig_atomic_t ConfigReloadPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index a5ee209f91..9eb50e9676 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -72,6 +72,7 @@ static void ShutdownPostgres(int code, Datum arg); static void StatementTimeoutHandler(void); static void LockTimeoutHandler(void); static void IdleInTransactionSessionTimeoutHandler(void); +static void CatcacheClockTimeoutHandler(void); static bool ThereIsAtLeastOneRole(void); static void process_startup_options(Port *port, bool am_superuser); static void process_settings(Oid databaseid, Oid roleid); @@ -628,6 +629,8 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username, RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler); RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT, IdleInTransactionSessionTimeoutHandler); + RegisterTimeout(CATCACHE_CLOCK_TIMEOUT, + CatcacheClockTimeoutHandler); } /* @@ -1238,6 +1241,14 @@ IdleInTransactionSessionTimeoutHandler(void) SetLatch(MyLatch); } +static void +CatcacheClockTimeoutHandler(void) +{ + CatcacheClockTimeoutPending = true; + InterruptPending = true; + SetLatch(MyLatch); +} + /* * Returns true if at least one role is defined in this database cluster. */ diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 156d147c85..d863c8dec8 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2205,6 +2206,28 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum unused duration of cache entries before removal."), + gettext_noop("Catalog cache entries that live unused for longer than this seconds are considered to be removed."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, assign_catalog_cache_prune_min_age, NULL + }, + + { + {"catalog_cache_memory_target", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum syscache size to keep."), + gettext_noop("Time-based cache pruning starts working after exceeding this size."), + GUC_UNIT_KB + }, + &catalog_cache_memory_target, + 0, 0, MAX_KILOBYTES, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 194f312096..7c82b0eca7 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,8 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_memory_target = 0kB # in kB +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index c9e35003a5..33b800e80f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -82,6 +82,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending; extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending; extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending; extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t CatcacheClockTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ConfigReloadPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..1ae49b4819 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,10 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + dlist_head cc_lru_list; + int cc_head_size; /* memory usage of catcache header */ + int cc_memusage; /* total memory usage of this catcache */ + int cc_nfreeent; /* # of entries currently not referenced */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,7 +124,10 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ - + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* approx. timestamp of the last usage */ + dlist_node lru_node; /* LRU node */ + int size; /* palloc'ed size off this tuple */ /* * The tuple may also be a member of at most one CatCList. (If a single * catcache is list-searched with varying numbers of keys, we may have to @@ -189,6 +197,39 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; +extern int catalog_cache_memory_target; +extern int catalog_cache_entry_limit; +extern double catalog_cache_prune_ratio; + +/* to use as access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* + * Flag to keep track of whether catcache timestamp timer is active. + */ +extern bool catcache_clock_timeout_active; + +/* catcache prune time helper functions */ +extern void SetupCatCacheClockTimer(void); +extern void UpdateCatCacheClock(void); + +/* + * SetCatCacheClock - set timestamp for catcache access record and start + * maintenance timer if needed. We keep to update the clock even while pruning + * is disable so that we are not confused by bogus clock value. + */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; + + if (!catcache_clock_timeout_active && catalog_cache_prune_min_age > 0) + SetupCatCacheClockTimer(); +} + +extern void assign_catalog_cache_prune_min_age(int newval, void *extra); extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h index 9244a2a7b7..b2d97b4f7b 100644 --- a/src/include/utils/timeout.h +++ b/src/include/utils/timeout.h @@ -31,6 +31,7 @@ typedef enum TimeoutId STANDBY_TIMEOUT, STANDBY_LOCK_TIMEOUT, IDLE_IN_TRANSACTION_SESSION_TIMEOUT, + CATCACHE_CLOCK_TIMEOUT, /* First user-definable timeout reason */ USER_TIMEOUT, /* Maximum number of timeout reasons */ -- 2.16.3 ----Next_Part(Wed_Feb_20_13_14_35_2019_032)---- ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 1/4] Remove entries that haven't been used for a certain time @ 2018-10-16 04:04 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw) Catcache entries can be left alone for several reasons. It is not desirable that they eat up memory. With this patch, This adds consideration of removal of entries that haven't been used for a certain time before enlarging the hash array. --- doc/src/sgml/config.sgml | 38 ++++++ src/backend/access/transam/xact.c | 5 + src/backend/utils/cache/catcache.c | 164 ++++++++++++++++++++++++-- src/backend/utils/misc/guc.c | 23 ++++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/utils/catcache.h | 28 ++++- 6 files changed, 252 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 9b7a7388d5..d0d2374944 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1662,6 +1662,44 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target"> + <term><varname>syscache_memory_target</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_memory_target</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum amount of memory to which syscache is expanded + without pruning. The value defaults to 0, indicating that pruning is + always considered. After exceeding this size, syscache pruning is + considered according to + <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep + certain amount of syscache entries with intermittent usage, try + increase this setting. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age"> + <term><varname>syscache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + syscache entry is considered to be removed. -1 indicates that syscache + pruning is disabled at all. The value defaults to 600 seconds + (<literal>10 minutes</literal>). The syscache entries that are not + used for the duration can be removed to prevent syscache bloat. This + behavior is suppressed until the size of syscache exceeds + <xref linkend="guc-syscache-memory-target"/>. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 92bda87804..ddc433c59e 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -734,7 +734,12 @@ void SetCurrentStatementStartTimestamp(void) { if (!IsParallelWorker()) + { stmtStartTimestamp = GetCurrentTimestamp(); + + /* Set this timestamp as aproximated current time */ + SetCatCacheClock(stmtStartTimestamp); + } else Assert(stmtStartTimestamp != 0); } diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 258a1d64cc..769e173844 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -71,9 +71,24 @@ #define CACHE6_elog(a,b,c,d,e,f,g) #endif +/* + * GUC variable to define the minimum size of hash to cosider entry eviction. + * This variable is shared among various cache mechanisms. + */ +int cache_memory_target = 0; + +/* GUC variable to define the minimum age of entries that will be cosidered to + * be evicted in seconds. This variable is shared among various cache + * mechanisms. + */ +int cache_prune_min_age = 600; + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Timestamp used for any operation on caches. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -490,6 +505,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, ct->keys); + cache->cc_tupsize -= ct->size; pfree(ct); --cache->cc_ntup; @@ -841,6 +857,7 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + cp->cc_tupsize = 0; /* * new cache is initialized as far as we can go for now. print some @@ -858,9 +875,127 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initilize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries can be left alone for several reasons. We remove them if + * they are not accessed for a certain time to prevent catcache from + * bloating. The eviction is performed with the similar algorithm with buffer + * eviction using access counter. Entries that are accessed several times can + * live longer than those that have had no access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int i; + int nremoved = 0; + size_t hash_size; +#ifdef CATCACHE_STATS + /* These variables are only for debugging purpose */ + int ntotal = 0; + /* + * nth element in nentries stores the number of cache entries that have + * lived unaccessed for corresponding multiple in ageclass of + * cache_prune_min_age. The index of nremoved_entry is the value of the + * clock-sweep counter, which takes from 0 up to 2. + */ + double ageclass[] = {0.05, 0.1, 1.0, 2.0, 3.0, 0.0}; + int nentries[] = {0, 0, 0, 0, 0, 0}; + int nremoved_entry[3] = {0, 0, 0}; + int j; +#endif + + /* Return immediately if no pruning is wanted */ + if (cache_prune_min_age < 0) + return false; + + /* + * Return without pruning if the size of the hash is below the target. + */ + hash_size = cp->cc_nbuckets * sizeof(dlist_head); + if (hash_size + cp->cc_tupsize < (Size) cache_memory_target * 1024L) + return false; + + /* Search the whole hash for entries to remove */ + for (i = 0; i < cp->cc_nbuckets; i++) + { + dlist_mutable_iter iter; + + dlist_foreach_modify(iter, &cp->cc_bucket[i]) + { + CatCTup *ct = dlist_container(CatCTup, cache_elem, iter.cur); + long entry_age; + int us; + + + /* + * Calculate the duration from the time of the last access to the + * "current" time. Since catcacheclock is not advanced within a + * transaction, the entries that are accessed within the current + * transaction won't be pruned. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + +#ifdef CATCACHE_STATS + /* count catcache entries for each age class */ + ntotal++; + for (j = 0 ; + ageclass[j] != 0.0 && + entry_age > cache_prune_min_age * ageclass[j] ; + j++); + if (ageclass[j] == 0.0) j--; + nentries[j]++; +#endif + + /* + * Try to remove entries older than cache_prune_min_age seconds. + * Entries that are not accessed after last pruning are removed in + * that seconds, and that has been accessed several times are + * removed after leaving alone for up to three times of the + * duration. We don't try shrink buckets since pruning effectively + * caps catcache expansion in the long term. + */ + if (entry_age > cache_prune_min_age) + { +#ifdef CATCACHE_STATS + Assert (ct->naccess >= 0 && ct->naccess <= 2); + nremoved_entry[ct->naccess]++; +#endif + if (ct->naccess > 0) + ct->naccess--; + else if (ct->refcount == 0 && + (!ct->c_list || ct->c_list->refcount == 0)) + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + } + } + +#ifdef CATCACHE_STATS + ereport(DEBUG1, + (errmsg ("removed %d/%d, age(-%.0fs:%d, -%.0fs:%d, *-%.0fs:%d, -%.0fs:%d, -%.0fs:%d) naccessed(0:%d, 1:%d, 2:%d)", + nremoved, ntotal, + ageclass[0] * cache_prune_min_age, nentries[0], + ageclass[1] * cache_prune_min_age, nentries[1], + ageclass[2] * cache_prune_min_age, nentries[2], + ageclass[3] * cache_prune_min_age, nentries[3], + ageclass[4] * cache_prune_min_age, nentries[4], + nremoved_entry[0], nremoved_entry[1], nremoved_entry[2]), + errhidestmt(true))); +#endif + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1274,6 +1409,11 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* Update access information for pruning */ + if (ct->naccess < 2) + ct->naccess++; + ct->lastaccess = catcacheclock; + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1819,11 +1959,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, CatCTup *ct; HeapTuple dtp; MemoryContext oldcxt; + int tupsize = 0; /* negative entries have no tuple associated */ if (ntp) { int i; + int tupsize; Assert(!negative); @@ -1842,13 +1984,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, /* Allocate memory for CatCTup and the cached tuple in one go */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + tupsize = sizeof(CatCTup) + MAXIMUM_ALIGNOF + dtp->t_len; + ct = (CatCTup *) palloc(tupsize); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; ct->tuple.t_data = (HeapTupleHeader) MAXALIGN(((char *) ct) + sizeof(CatCTup)); + ct->size = tupsize; /* copy tuple contents */ memcpy((char *) ct->tuple.t_data, (const char *) dtp->t_data, @@ -1876,8 +2019,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, { Assert(negative); oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup)); - + tupsize = sizeof(CatCTup); + ct = (CatCTup *) palloc(tupsize); /* * Store keys - they'll point into separately allocated memory if not * by-value. @@ -1898,17 +2041,24 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + ct->size = tupsize; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + cache->cc_tupsize += tupsize; /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try cleanup by removing + * infrequently used entries to make a room for the new entry. If it + * failed, enlarge the bucket array instead. Quite arbitrarily, we try + * this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); return ct; diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 8681ada33a..06c589f725 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2204,6 +2205,28 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"cache_memory_target", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum syscache size to keep."), + gettext_noop("Cache is not pruned before exceeding this size."), + GUC_UNIT_KB + }, + &cache_memory_target, + 0, 0, MAX_KILOBYTES, + NULL, NULL, NULL + }, + + { + {"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum unused duration of cache entries before removal."), + gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."), + GUC_UNIT_S + }, + &cache_prune_min_age, + 600, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index c7f53470df..108d332f2c 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,8 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#cache_memory_target = 0kB # in kB +#cache_prune_min_age = 600s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..5d24809900 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,7 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + int cc_tupsize; /* total amount of catcache tuples */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,7 +121,9 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ - + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* approx. timestamp of the last usage */ + int size; /* palloc'ed size off this tuple */ /* * The tuple may also be a member of at most one CatCList. (If a single * catcache is list-searched with varying numbers of keys, we may have to @@ -189,6 +193,28 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int cache_prune_min_age; +extern int cache_memory_target; + +/* to use as access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* + * SetCatCacheClock - set timestamp for catcache access record + */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + +static inline TimestampTz +GetCatCacheClock(void) +{ + return catcacheclock; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Wed_Feb_06_15_17_59_2019_321)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v10-0002-Syscache-usage-tracking-feature.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 2/3] Remove entries that haven't been used for a certain time @ 2018-10-16 04:04 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw) Catcache entries can be left alone for several reasons. It is not desirable that they eat up memory. With this patch, This adds consideration of removal of entries that haven't been used for a certain time before enlarging the hash array. This also can put a hard limit on the number of catcache entries. --- doc/src/sgml/config.sgml | 41 ++++ src/backend/tcop/postgres.c | 13 ++ src/backend/utils/cache/catcache.c | 285 +++++++++++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 11 + src/backend/utils/misc/guc.c | 43 ++++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/miscadmin.h | 1 + src/include/utils/catcache.h | 49 ++++- src/include/utils/timeout.h | 1 + 10 files changed, 440 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 07b847a8e9..71d784b6fe 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1661,6 +1661,47 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + + Specifies the minimum amount of unused time in seconds at which a + catalog cache entry is considered to be removed. -1 indicates that + this feature is disabled at all. The value defaults to 300 seconds + (<literal>5 minutes</literal>). The catalog cache entries that are + not used for the duration can be removed to prevent bloat. This + behavior is suppressed until the size of a catalog cache exceeds + <xref linkend="guc-catalog-cache-memory-target"/>. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-catalog-cache-memory-target" xreflabel="catalog_cache_memory_target"> + <term><varname>catalog_cache_memory_target</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_memory_target</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum amount of memory to which syscache is expanded + without pruning in kilobytes. The value defaults to 0, indicating that + pruning is always considered. After exceeding this size, catalog cache + pruning is considered according to + <xref linkend="guc-catalog-cache-prune-min-age"/>. If you need to keep + certain amount of catalog cache entries with intermittent usage, try + increase this setting. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 36cfd507b2..f192ee2ca6 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2584,6 +2585,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void @@ -3159,6 +3161,14 @@ ProcessInterrupts(void) if (ParallelMessagePending) HandleParallelMessages(); + + if (CatcacheClockTimeoutPending) + { + CatcacheClockTimeoutPending = 0; + + /* Update timetamp then set up the next timeout */ + UpdateCatCacheClock(); + } } @@ -4021,6 +4031,9 @@ PostgresMain(int argc, char *argv[], QueryCancelPending = false; /* second to avoid race condition */ stmt_timeout_active = false; + /* get sync with the timer state */ + catcache_clock_timeout_active = false; + /* Not reading from the client anymore. */ DoingCommandRead = false; diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 258a1d64cc..0195e19976 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -39,6 +39,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -71,9 +72,43 @@ #define CACHE6_elog(a,b,c,d,e,f,g) #endif +/* GUC variable to define the minimum age of entries that will be considered to + * be evicted in seconds. This variable is shared among various cache + * mechanisms. + */ +int catalog_cache_prune_min_age = 300; + +/* + * GUC variable to define the minimum size of hash to cosider entry eviction. + * This variable is shared among various cache mechanisms. + */ +int catalog_cache_memory_target = 0; + +/* + * GUC for limit by the number of entries. Entries are removed when the number + * of them goes above catalog_cache_entry_limit and leaving newer entries by + * the ratio specified by catalog_cache_prune_ratio. + */ +int catalog_cache_entry_limit = 0; +double catalog_cache_prune_ratio = 0.8; + +/* + * Flag to keep track of whether catcache clock timer is active. + */ +bool catcache_clock_timeout_active = false; + +/* + * Minimum interval between two success move of a cache entry in LRU list, + * in microseconds. + */ +#define MIN_LRU_UPDATE_INTERVAL 100000 /* 100ms */ + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock used to record the last accessed time of a catcache record. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -481,6 +516,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) /* delink from linked list */ dlist_delete(&ct->cache_elem); + dlist_delete(&ct->lru_node); /* * Free keys when we're dealing with a negative entry, normal entries just @@ -490,6 +526,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, ct->keys); + cache->cc_memusage -= ct->size; pfree(ct); --cache->cc_ntup; @@ -841,7 +878,13 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + cp->cc_memusage = + CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext, + cp) + + CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext, + cp->cc_bucket); + dlist_init(&cp->cc_lru_list); /* * new cache is initialized as far as we can go for now. print some * debugging information, if appropriate. @@ -858,9 +901,191 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * helper routine for SetCatCacheClock and UpdateCatCacheClockTimer. + * + * We need to maintain the catcache clock during a long query. + */ +void +SetupCatCacheClockTimer(void) +{ + long delay; + + /* stop timer if not needed */ + if (catalog_cache_prune_min_age == 0) + { + catcache_clock_timeout_active = false; + return; + } + + /* One 10th of the variable, in milliseconds */ + delay = catalog_cache_prune_min_age * 1000/10; + + /* Lower limit is 1 second */ + if (delay < 1000) + delay = 1000; + + enable_timeout_after(CATCACHE_CLOCK_TIMEOUT, delay); + + catcache_clock_timeout_active = true; +} + +/* + * Update catcacheclock: this is intended to be called from + * CATCACHE_CLOCK_TIMEOUT. The interval is expected more than 1 second (see + * above), so GetCurrentTime() doesn't harm. + */ +void +UpdateCatCacheClock(void) +{ + catcacheclock = GetCurrentTimestamp(); + SetupCatCacheClockTimer(); +} + +/* + * It may take an unexpectedly long time before the next clock update when + * catalog_cache_prune_min_age gets shorter. Disabling the current timer let + * the next update happen at the expected interval. We don't necessariry + * require this for increase the age but we don't need to avoid to disable + * either. + */ +void +assign_catalog_cache_prune_min_age(int newval, void *extra) +{ + if (catcache_clock_timeout_active) + disable_timeout(CATCACHE_CLOCK_TIMEOUT, false); + + catcache_clock_timeout_active = false; +} + +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries can be left alone for several reasons. We remove them if + * they are not accessed for a certain time to prevent catcache from + * bloating. The eviction is performed with the similar algorithm with buffer + * eviction using access counter. Entries that are accessed several times can + * live longer than those that have had less access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + size_t hash_size; + int nelems_before = cp->cc_ntup; + int ndelelems = 0; + bool prune_by_age = false; + bool prune_by_number = false; + dlist_mutable_iter iter; + + if (catalog_cache_prune_min_age >= 0) + { + /* prune only if the size of the hash is above the target */ + + hash_size = cp->cc_nbuckets * sizeof(dlist_head); + if (hash_size + cp->cc_memusage > + (Size) catalog_cache_memory_target * 1024L) + prune_by_age = true; + } + + if (catalog_cache_entry_limit > 0 && + nelems_before >= catalog_cache_entry_limit) + { + ndelelems = nelems_before - + (int) (catalog_cache_entry_limit * catalog_cache_prune_ratio); + + /* an arbitrary lower limit.. */ + if (ndelelems < 256) + ndelelems = 256; + if (ndelelems > nelems_before) + ndelelems = nelems_before; + + prune_by_number = true; + } + + /* Return immediately if no pruning is wanted */ + if (!prune_by_age && !prune_by_number) + return false; + + /* Scan over LRU to find entries to remove */ + dlist_foreach_modify(iter, &cp->cc_lru_list) + { + CatCTup *ct = dlist_container(CatCTup, lru_node, iter.cur); + bool remove_this = false; + + /* We don't remove referenced entry */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* check against age */ + if (prune_by_age) + { + long entry_age; + int us; + + /* + * Calculate the duration from the time of the last access to the + * "current" time. Since catcacheclock is not advanced within a + * transaction, the entries that are accessed within the current + * transaction won't be pruned. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < catalog_cache_prune_min_age) + { + /* no longer have a business with further entries, exit */ + prune_by_age = false; + break; + } + /* + * Entries that are not accessed after last pruning are removed in + * that seconds, and that has been accessed several times are + * removed after leaving alone for up to three times of the + * duration. We don't try shrink buckets since pruning effectively + * caps catcache expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + remove_this = true; + } + + /* check against entry number */ + if (prune_by_number) + { + if (nremoved < ndelelems) + remove_this = true; + else + prune_by_number = false; /* we're satisfied */ + } + + /* exit immediately if all finished */ + if (!prune_by_age && !prune_by_number) + break; + + /* do the work */ + if (remove_this) + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, nelems_before); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -878,6 +1103,13 @@ RehashCatCache(CatCache *cp) newnbuckets = cp->cc_nbuckets * 2; newbucket = (dlist_head *) MemoryContextAllocZero(CacheMemoryContext, newnbuckets * sizeof(dlist_head)); + /* recalculate memory usage from the first */ + cp->cc_memusage = + CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext, + cp) + + CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext, + newbucket); + /* Move all entries from old hash table to new. */ for (i = 0; i < cp->cc_nbuckets; i++) { @@ -890,6 +1122,7 @@ RehashCatCache(CatCache *cp) dlist_delete(iter.cur); dlist_push_head(&newbucket[hashIndex], &ct->cache_elem); + cp->cc_memusage += ct->size; } } @@ -1274,6 +1507,21 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* Update access information for pruning */ + if (ct->naccess < 2) + ct->naccess++; + + /* + * We don't want too frequent update of + * LRU. catalog_cache_prune_min_age can be changed on-session so we + * need to maintain the LRU regardless of catalog_cache_prune_min_age. + */ + if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL) + { + ct->lastaccess = catcacheclock; + dlist_move_tail(&cache->cc_lru_list, &ct->lru_node); + } + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1709,6 +1957,11 @@ SearchCatCacheList(CatCache *cache, /* Now we can build the CatCList entry. */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); nmembers = list_length(ctlist); + + /* + * Don't waste a time by counting the list in catcache memory usage, + * since it doesn't live a long life. + */ cl = (CatCList *) palloc(offsetof(CatCList, members) + nmembers * sizeof(CatCTup *)); @@ -1824,6 +2077,7 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, if (ntp) { int i; + int tupsize; Assert(!negative); @@ -1842,8 +2096,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, /* Allocate memory for CatCTup and the cached tuple in one go */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + tupsize = sizeof(CatCTup) + MAXIMUM_ALIGNOF + dtp->t_len; + ct = (CatCTup *) palloc(tupsize); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; @@ -1877,7 +2131,6 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, Assert(negative); oldcxt = MemoryContextSwitchTo(CacheMemoryContext); ct = (CatCTup *) palloc(sizeof(CatCTup)); - /* * Store keys - they'll point into separately allocated memory if not * by-value. @@ -1898,18 +2151,38 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + dlist_push_tail(&cache->cc_lru_list, &ct->lru_node); dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + ct->size = + CacheMemoryContext->methods->get_chunk_space(CacheMemoryContext, + ct); + cache->cc_memusage += ct->size; + + /* increase refcount so that this survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try cleanup by removing + * infrequently used entries to make a room for the new entry. If it + * failed, enlarge the bucket array instead. Quite arbitrarily, we try + * this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + /* we may still want to prune by entry number, check it */ + else if (catalog_cache_entry_limit > 0 && + cache->cc_ntup > catalog_cache_entry_limit) + CatCacheCleanupOldEntries(cache); + + ct->refcount--; return ct; } diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index fd51934aaf..0e8b972a29 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -32,6 +32,7 @@ volatile sig_atomic_t QueryCancelPending = false; volatile sig_atomic_t ProcDiePending = false; volatile sig_atomic_t ClientConnectionLost = false; volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false; +volatile sig_atomic_t CatcacheClockTimeoutPending = false; volatile sig_atomic_t ConfigReloadPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index a5ee209f91..9eb50e9676 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -72,6 +72,7 @@ static void ShutdownPostgres(int code, Datum arg); static void StatementTimeoutHandler(void); static void LockTimeoutHandler(void); static void IdleInTransactionSessionTimeoutHandler(void); +static void CatcacheClockTimeoutHandler(void); static bool ThereIsAtLeastOneRole(void); static void process_startup_options(Port *port, bool am_superuser); static void process_settings(Oid databaseid, Oid roleid); @@ -628,6 +629,8 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username, RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler); RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT, IdleInTransactionSessionTimeoutHandler); + RegisterTimeout(CATCACHE_CLOCK_TIMEOUT, + CatcacheClockTimeoutHandler); } /* @@ -1238,6 +1241,14 @@ IdleInTransactionSessionTimeoutHandler(void) SetLatch(MyLatch); } +static void +CatcacheClockTimeoutHandler(void) +{ + CatcacheClockTimeoutPending = true; + InterruptPending = true; + SetLatch(MyLatch); +} + /* * Returns true if at least one role is defined in this database cluster. */ diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 41d477165c..c62d5ad8b8 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2205,6 +2206,38 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum unused duration of cache entries before removal."), + gettext_noop("Catalog cache entries that live unused for longer than this seconds are considered to be removed."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, assign_catalog_cache_prune_min_age, NULL + }, + + { + {"catalog_cache_memory_target", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum syscache size to keep."), + gettext_noop("Time-based cache pruning starts working after exceeding this size."), + GUC_UNIT_KB + }, + &catalog_cache_memory_target, + 0, 0, MAX_KILOBYTES, + NULL, NULL, NULL + }, + + { + {"catalog_cache_entry_limit", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &catalog_cache_entry_limit, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if @@ -3368,6 +3401,16 @@ static struct config_real ConfigureNamesReal[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_ratio", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Reduce ratio of pruning caused by catalog_cache_entry_limit."), + NULL + }, + &catalog_cache_prune_ratio, + 0.8, 0.0, 1.0, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index ad6c436f93..aeb5968e75 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,8 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_memory_target = 0kB # in kB +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index c9e35003a5..33b800e80f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -82,6 +82,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending; extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending; extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending; extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t CatcacheClockTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ConfigReloadPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..0425fc0786 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,10 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + dlist_head cc_lru_list; + int cc_memusage; /* memory usage of this catcache (excluding + * header part) */ + int cc_nfreeent; /* # of entries currently not referenced */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,7 +124,10 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ - + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* approx. timestamp of the last usage */ + dlist_node lru_node; /* LRU node */ + int size; /* palloc'ed size off this tuple */ /* * The tuple may also be a member of at most one CatCList. (If a single * catcache is list-searched with varying numbers of keys, we may have to @@ -189,6 +197,45 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; +extern int catalog_cache_memory_target; +extern int catalog_cache_entry_limit; +extern double catalog_cache_prune_ratio; + +/* to use as access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* + * Flag to keep track of whether catcache timestamp timer is active. + */ +extern bool catcache_clock_timeout_active; + +/* catcache prune time helper functions */ +extern void SetupCatCacheClockTimer(void); +extern void UpdateCatCacheClock(void); + +/* + * SetCatCacheClock - set timestamp for catcache access record and start + * maintenance timer if needed. We keep to update the clock even while pruning + * is disable so that we are not confused by bogus clock value. + */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; + + if (!catcache_clock_timeout_active && catalog_cache_prune_min_age > 0) + SetupCatCacheClockTimer(); +} + +static inline TimestampTz +GetCatCacheClock(void) +{ + return catcacheclock; +} + +extern void assign_catalog_cache_prune_min_age(int newval, void *extra); extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h index 9244a2a7b7..b2d97b4f7b 100644 --- a/src/include/utils/timeout.h +++ b/src/include/utils/timeout.h @@ -31,6 +31,7 @@ typedef enum TimeoutId STANDBY_TIMEOUT, STANDBY_LOCK_TIMEOUT, IDLE_IN_TRANSACTION_SESSION_TIMEOUT, + CATCACHE_CLOCK_TIMEOUT, /* First user-definable timeout reason */ USER_TIMEOUT, /* Maximum number of timeout reasons */ -- 2.16.3 ----Next_Part(Tue_Feb_12_20_36_28_2019_578)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v14-0003-Syscache-usage-tracking-feature.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 2/4] Remove entries that haven't been used for a certain time @ 2018-10-16 04:04 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw) Catcache entries can be left alone for several reasons. It is not desirable that they eat up memory. With this patch, This adds consideration of removal of entries that haven't been used for a certain time before enlarging the hash array. This also can put a hard limit on the number of catcache entries. --- doc/src/sgml/config.sgml | 38 ++++++ src/backend/access/transam/xact.c | 5 + src/backend/utils/cache/catcache.c | 190 +++++++++++++++++++++++++- src/backend/utils/misc/guc.c | 63 +++++++++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/utils/catcache.h | 32 ++++- 6 files changed, 322 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 9b7a7388d5..d0d2374944 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1662,6 +1662,44 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target"> + <term><varname>syscache_memory_target</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_memory_target</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum amount of memory to which syscache is expanded + without pruning. The value defaults to 0, indicating that pruning is + always considered. After exceeding this size, syscache pruning is + considered according to + <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep + certain amount of syscache entries with intermittent usage, try + increase this setting. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age"> + <term><varname>syscache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + syscache entry is considered to be removed. -1 indicates that syscache + pruning is disabled at all. The value defaults to 600 seconds + (<literal>10 minutes</literal>). The syscache entries that are not + used for the duration can be removed to prevent syscache bloat. This + behavior is suppressed until the size of syscache exceeds + <xref linkend="guc-syscache-memory-target"/>. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 92bda87804..ddc433c59e 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -734,7 +734,12 @@ void SetCurrentStatementStartTimestamp(void) { if (!IsParallelWorker()) + { stmtStartTimestamp = GetCurrentTimestamp(); + + /* Set this timestamp as aproximated current time */ + SetCatCacheClock(stmtStartTimestamp); + } else Assert(stmtStartTimestamp != 0); } diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 258a1d64cc..0a56390352 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -71,9 +71,32 @@ #define CACHE6_elog(a,b,c,d,e,f,g) #endif +/* + * GUC variable to define the minimum size of hash to cosider entry eviction. + * This variable is shared among various cache mechanisms. + */ +int cache_memory_target = 0; + + +/* + * GUC for entry limit. Entries are removed when the number of them goes above + * cache_entry_limit by the ratio specified by cache_entry_limit_prune_ratio + */ +int cache_entry_limit = 0; +double cache_entry_limit_prune_ratio = 0.8; + +/* GUC variable to define the minimum age of entries that will be cosidered to + * be evicted in seconds. This variable is shared among various cache + * mechanisms. + */ +int cache_prune_min_age = 600; + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Timestamp used for any operation on caches. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -481,6 +504,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) /* delink from linked list */ dlist_delete(&ct->cache_elem); + dlist_delete(&ct->lru_node); /* * Free keys when we're dealing with a negative entry, normal entries just @@ -490,6 +514,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, ct->keys); + cache->cc_tupsize -= ct->size; pfree(ct); --cache->cc_ntup; @@ -841,7 +866,9 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + cp->cc_tupsize = 0; + dlist_init(&cp->cc_lru_list); /* * new cache is initialized as far as we can go for now. print some * debugging information, if appropriate. @@ -858,9 +885,133 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initilize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries can be left alone for several reasons. We remove them if + * they are not accessed for a certain time to prevent catcache from + * bloating. The eviction is performed with the similar algorithm with buffer + * eviction using access counter. Entries that are accessed several times can + * live longer than those that have had no access in the same duration. + */ +#define PRUNE_BY_AGE 0x01 +#define PRUNE_BY_NUMBER 0x02 + +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + size_t hash_size; + int nelems_before = cp->cc_ntup; + int ndelelems = 0; + int action = 0; + dlist_mutable_iter iter; + + if (cache_prune_min_age >= 0) + { + /* prune only if the size of the hash is above the target */ + + hash_size = cp->cc_nbuckets * sizeof(dlist_head); + if (hash_size + cp->cc_tupsize > (Size) cache_memory_target * 1024L) + action |= PRUNE_BY_AGE; + } + + if (cache_entry_limit > 0 && nelems_before >= cache_entry_limit) + { + ndelelems = nelems_before - + (int) (cache_entry_limit * cache_entry_limit_prune_ratio); + + if (ndelelems < 256) + ndelelems = 256; + if (ndelelems > nelems_before) + ndelelems = nelems_before; + + action |= PRUNE_BY_NUMBER; + } + + /* Return immediately if no pruning is wanted */ + if (action == 0) + return false; + + /* Scan over LRU to find entries to remove */ + dlist_foreach_modify(iter, &cp->cc_lru_list) + { + CatCTup *ct = dlist_container(CatCTup, lru_node, iter.cur); + bool remove_this = false; + + /* We don't remove referenced entry */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* check against age */ + if (action & PRUNE_BY_AGE) + { + long entry_age; + int us; + + /* + * Calculate the duration from the time of the last access to the + * "current" time. Since catcacheclock is not advanced within a + * transaction, the entries that are accessed within the current + * transaction won't be pruned. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < cache_prune_min_age) + { + /* no longer have a business with further entries, exit */ + action &= ~PRUNE_BY_AGE; + break; + } + + /* + * Entries that are not accessed after last pruning are removed in + * that seconds, and that has been accessed several times are + * removed after leaving alone for up to three times of the + * duration. We don't try shrink buckets since pruning effectively + * caps catcache expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + remove_this = true; + } + + /* check against entry number */ + if (action & PRUNE_BY_NUMBER) + { + if (nremoved < ndelelems) + remove_this = true; + else + action &= ~PRUNE_BY_NUMBER; /* satisfied */ + } + + /* exit if finished */ + if (action == 0) + break; + + /* do the work */ + if (remove_this) + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, nelems_before); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1274,6 +1425,12 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* Update access information for pruning */ + if (ct->naccess < 2) + ct->naccess++; + ct->lastaccess = catcacheclock; + dlist_move_tail(&cache->cc_lru_list, &ct->lru_node); + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1819,11 +1976,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, CatCTup *ct; HeapTuple dtp; MemoryContext oldcxt; + int tupsize = 0; /* negative entries have no tuple associated */ if (ntp) { int i; + int tupsize; Assert(!negative); @@ -1842,13 +2001,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, /* Allocate memory for CatCTup and the cached tuple in one go */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + tupsize = sizeof(CatCTup) + MAXIMUM_ALIGNOF + dtp->t_len; + ct = (CatCTup *) palloc(tupsize); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; ct->tuple.t_data = (HeapTupleHeader) MAXALIGN(((char *) ct) + sizeof(CatCTup)); + ct->size = tupsize; /* copy tuple contents */ memcpy((char *) ct->tuple.t_data, (const char *) dtp->t_data, @@ -1876,8 +2036,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, { Assert(negative); oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup)); - + tupsize = sizeof(CatCTup); + ct = (CatCTup *) palloc(tupsize); /* * Store keys - they'll point into separately allocated memory if not * by-value. @@ -1898,18 +2058,34 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + dlist_push_tail(&cache->cc_lru_list, &ct->lru_node); + ct->size = tupsize; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + cache->cc_tupsize += tupsize; + + /* increase refcount so that this survives pruning */ + ct->refcount++; /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try cleanup by removing + * infrequently used entries to make a room for the new entry. If it + * failed, enlarge the bucket array instead. Quite arbitrarily, we try + * this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + /* we may still want to prune by entry number, check it */ + else if (cache_entry_limit > 0 && cache->cc_ntup > cache_entry_limit) + CatCacheCleanupOldEntries(cache); + + ct->refcount--; return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 8681ada33a..d4df841982 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2204,6 +2205,58 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"cache_memory_target", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum syscache size to keep."), + gettext_noop("Cache is not pruned before exceeding this size."), + GUC_UNIT_KB + }, + &cache_memory_target, + 0, 0, MAX_KILOBYTES, + NULL, NULL, NULL + }, + + { + {"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum unused duration of cache entries before removal."), + gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."), + GUC_UNIT_S + }, + &cache_prune_min_age, + 600, -1, INT_MAX, + NULL, NULL, NULL + }, + + { + {"cache_entry_limit", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &cache_entry_limit, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + + { + {"cache_entry_limit", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &cache_entry_limit, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + + { + {"cache_entry_limit", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &cache_entry_limit, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if @@ -3368,6 +3421,16 @@ static struct config_real ConfigureNamesReal[] = NULL, NULL, NULL }, + { + {"cache_entry_limit_prune_ratio", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &cache_entry_limit_prune_ratio, + 0.8, 0.0, 1.0, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index c7f53470df..108d332f2c 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,8 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#cache_memory_target = 0kB # in kB +#cache_prune_min_age = 600s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..973a87c2cf 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,8 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + dlist_head cc_lru_list; + int cc_tupsize; /* total amount of catcache tuples */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,7 +122,10 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ - + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* approx. timestamp of the last usage */ + dlist_node lru_node; /* LRU node */ + int size; /* palloc'ed size off this tuple */ /* * The tuple may also be a member of at most one CatCList. (If a single * catcache is list-searched with varying numbers of keys, we may have to @@ -189,6 +195,30 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int cache_prune_min_age; +extern int cache_memory_target; +extern int cache_entry_limit; +extern double cache_entry_limit_prune_ratio; + +/* to use as access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* + * SetCatCacheClock - set timestamp for catcache access record + */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + +static inline TimestampTz +GetCatCacheClock(void) +{ + return catcacheclock; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Thu_Feb_07_15_24_18_2019_171)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v12-0003-Syscache-usage-tracking-feature.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 1/4] Remove entries that haven't been used for a certain time @ 2018-10-16 04:04 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw) Catcache entries can be left alone for several reasons. It is not desirable that they eat up memory. With this patch, This adds consideration of removal of entries that haven't been used for a certain time before enlarging the hash array. --- doc/src/sgml/config.sgml | 38 ++++++ src/backend/access/transam/xact.c | 5 + src/backend/utils/cache/catcache.c | 166 ++++++++++++++++++++++++-- src/backend/utils/misc/guc.c | 23 ++++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/utils/catcache.h | 28 ++++- 6 files changed, 254 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 9b7a7388d5..d0d2374944 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1662,6 +1662,44 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target"> + <term><varname>syscache_memory_target</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_memory_target</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum amount of memory to which syscache is expanded + without pruning. The value defaults to 0, indicating that pruning is + always considered. After exceeding this size, syscache pruning is + considered according to + <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep + certain amount of syscache entries with intermittent usage, try + increase this setting. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age"> + <term><varname>syscache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + syscache entry is considered to be removed. -1 indicates that syscache + pruning is disabled at all. The value defaults to 600 seconds + (<literal>10 minutes</literal>). The syscache entries that are not + used for the duration can be removed to prevent syscache bloat. This + behavior is suppressed until the size of syscache exceeds + <xref linkend="guc-syscache-memory-target"/>. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 92bda87804..ddc433c59e 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -734,7 +734,12 @@ void SetCurrentStatementStartTimestamp(void) { if (!IsParallelWorker()) + { stmtStartTimestamp = GetCurrentTimestamp(); + + /* Set this timestamp as aproximated current time */ + SetCatCacheClock(stmtStartTimestamp); + } else Assert(stmtStartTimestamp != 0); } diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 258a1d64cc..2a996d740a 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -71,9 +71,24 @@ #define CACHE6_elog(a,b,c,d,e,f,g) #endif +/* + * GUC variable to define the minimum size of hash to cosider entry eviction. + * This variable is shared among various cache mechanisms. + */ +int cache_memory_target = 0; + +/* GUC variable to define the minimum age of entries that will be cosidered to + * be evicted in seconds. This variable is shared among various cache + * mechanisms. + */ +int cache_prune_min_age = 600; + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Timestamp used for any operation on caches. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -490,6 +505,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, ct->keys); + cache->cc_tupsize -= ct->size; pfree(ct); --cache->cc_ntup; @@ -841,6 +857,7 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + cp->cc_tupsize = 0; /* * new cache is initialized as far as we can go for now. print some @@ -858,9 +875,129 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initilize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries can be left alone for several reasons. We remove them if + * they are not accessed for a certain time to prevent catcache from + * bloating. The eviction is performed with the similar algorithm with buffer + * eviction using access counter. Entries that are accessed several times can + * live longer than those that have had no access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int i; + int nremoved = 0; + size_t hash_size; +#ifdef CATCACHE_STATS + /* These variables are only for debugging purpose */ + int ntotal = 0; + /* + * nth element in nentries stores the number of cache entries that have + * lived unaccessed for corresponding multiple in ageclass of + * cache_prune_min_age. The index of nremoved_entry is the value of the + * clock-sweep counter, which takes from 0 up to 2. + */ + double ageclass[] = {0.05, 0.1, 1.0, 2.0, 3.0, 0.0}; + int nentries[] = {0, 0, 0, 0, 0, 0}; + int nremoved_entry[3] = {0, 0, 0}; + int j; +#endif + + /* Return immediately if no pruning is wanted */ + if (cache_prune_min_age < 0) + return false; + + /* + * Return without pruning if the size of the hash is below the target. + */ + hash_size = cp->cc_nbuckets * sizeof(dlist_head); + if (hash_size + cp->cc_tupsize < (Size) cache_memory_target * 1024L) + return false; + + /* Search the whole hash for entries to remove */ + for (i = 0; i < cp->cc_nbuckets; i++) + { + dlist_mutable_iter iter; + + dlist_foreach_modify(iter, &cp->cc_bucket[i]) + { + CatCTup *ct = dlist_container(CatCTup, cache_elem, iter.cur); + long entry_age; + int us; + + + /* + * Calculate the duration from the time of the last access to the + * "current" time. Since catcacheclock is not advanced within a + * transaction, the entries that are accessed within the current + * transaction won't be pruned. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + +#ifdef CATCACHE_STATS + /* count catcache entries for each age class */ + ntotal++; + for (j = 0 ; + ageclass[j] != 0.0 && + entry_age > cache_prune_min_age * ageclass[j] ; + j++); + if (ageclass[j] == 0.0) j--; + nentries[j]++; +#endif + + /* + * Try to remove entries older than cache_prune_min_age seconds. + * Entries that are not accessed after last pruning are removed in + * that seconds, and that has been accessed several times are + * removed after leaving alone for up to three times of the + * duration. We don't try shrink buckets since pruning effectively + * caps catcache expansion in the long term. + */ + if (entry_age > cache_prune_min_age) + { +#ifdef CATCACHE_STATS + Assert (ct->naccess >= 0 && ct->naccess <= 2); + nremoved_entry[ct->naccess]++; +#endif + if (ct->naccess > 0) + ct->naccess--; + else + { + if (!ct->c_list || ct->c_list->refcount == 0) + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + } + } + } + +#ifdef CATCACHE_STATS + ereport(DEBUG1, + (errmsg ("removed %d/%d, age(-%.0fs:%d, -%.0fs:%d, *-%.0fs:%d, -%.0fs:%d, -%.0fs:%d) naccessed(0:%d, 1:%d, 2:%d)", + nremoved, ntotal, + ageclass[0] * cache_prune_min_age, nentries[0], + ageclass[1] * cache_prune_min_age, nentries[1], + ageclass[2] * cache_prune_min_age, nentries[2], + ageclass[3] * cache_prune_min_age, nentries[3], + ageclass[4] * cache_prune_min_age, nentries[4], + nremoved_entry[0], nremoved_entry[1], nremoved_entry[2]), + errhidestmt(true))); +#endif + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1274,6 +1411,11 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* Update access information for pruning */ + if (ct->naccess < 2) + ct->naccess++; + ct->lastaccess = catcacheclock; + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1819,11 +1961,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, CatCTup *ct; HeapTuple dtp; MemoryContext oldcxt; + int tupsize = 0; /* negative entries have no tuple associated */ if (ntp) { int i; + int tupsize; Assert(!negative); @@ -1842,13 +1986,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, /* Allocate memory for CatCTup and the cached tuple in one go */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + tupsize = sizeof(CatCTup) + MAXIMUM_ALIGNOF + dtp->t_len; + ct = (CatCTup *) palloc(tupsize); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; ct->tuple.t_data = (HeapTupleHeader) MAXALIGN(((char *) ct) + sizeof(CatCTup)); + ct->size = tupsize; /* copy tuple contents */ memcpy((char *) ct->tuple.t_data, (const char *) dtp->t_data, @@ -1876,8 +2021,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, { Assert(negative); oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup)); - + tupsize = sizeof(CatCTup); + ct = (CatCTup *) palloc(tupsize); /* * Store keys - they'll point into separately allocated memory if not * by-value. @@ -1898,17 +2043,24 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + ct->size = tupsize; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + cache->cc_tupsize += tupsize; /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try cleanup by removing + * infrequently used entries to make a room for the new entry. If it + * failed, enlarge the bucket array instead. Quite arbitrarily, we try + * this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); return ct; diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 8681ada33a..06c589f725 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2204,6 +2205,28 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"cache_memory_target", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum syscache size to keep."), + gettext_noop("Cache is not pruned before exceeding this size."), + GUC_UNIT_KB + }, + &cache_memory_target, + 0, 0, MAX_KILOBYTES, + NULL, NULL, NULL + }, + + { + {"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum unused duration of cache entries before removal."), + gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."), + GUC_UNIT_S + }, + &cache_prune_min_age, + 600, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index c7f53470df..108d332f2c 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,8 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#cache_memory_target = 0kB # in kB +#cache_prune_min_age = 600s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..5d24809900 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,7 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + int cc_tupsize; /* total amount of catcache tuples */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,7 +121,9 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ - + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* approx. timestamp of the last usage */ + int size; /* palloc'ed size off this tuple */ /* * The tuple may also be a member of at most one CatCList. (If a single * catcache is list-searched with varying numbers of keys, we may have to @@ -189,6 +193,28 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int cache_prune_min_age; +extern int cache_memory_target; + +/* to use as access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* + * SetCatCacheClock - set timestamp for catcache access record + */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + +static inline TimestampTz +GetCatCacheClock(void) +{ + return catcacheclock; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Wed_Feb_06_14_43_34_2019_896)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v9-0002-Syscache-usage-tracking-feature.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 2/4] Remove entries that haven't been used for a certain time @ 2018-10-16 04:04 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw) Catcache entries can be left alone for several reasons. It is not desirable that they eat up memory. With this patch, This adds consideration of removal of entries that haven't been used for a certain time before enlarging the hash array. This also can put a hard limit on the number of catcache entries. --- doc/src/sgml/config.sgml | 38 +++++ src/backend/access/transam/xact.c | 5 + src/backend/utils/cache/catcache.c | 205 +++++++++++++++++++++++++- src/backend/utils/misc/guc.c | 63 ++++++++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/utils/catcache.h | 33 ++++- 6 files changed, 338 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 9b7a7388d5..d0d2374944 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1662,6 +1662,44 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target"> + <term><varname>syscache_memory_target</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_memory_target</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum amount of memory to which syscache is expanded + without pruning. The value defaults to 0, indicating that pruning is + always considered. After exceeding this size, syscache pruning is + considered according to + <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep + certain amount of syscache entries with intermittent usage, try + increase this setting. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age"> + <term><varname>syscache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + syscache entry is considered to be removed. -1 indicates that syscache + pruning is disabled at all. The value defaults to 600 seconds + (<literal>10 minutes</literal>). The syscache entries that are not + used for the duration can be removed to prevent syscache bloat. This + behavior is suppressed until the size of syscache exceeds + <xref linkend="guc-syscache-memory-target"/>. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 92bda87804..ddc433c59e 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -734,7 +734,12 @@ void SetCurrentStatementStartTimestamp(void) { if (!IsParallelWorker()) + { stmtStartTimestamp = GetCurrentTimestamp(); + + /* Set this timestamp as aproximated current time */ + SetCatCacheClock(stmtStartTimestamp); + } else Assert(stmtStartTimestamp != 0); } diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 258a1d64cc..c70ce3b745 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -71,9 +71,38 @@ #define CACHE6_elog(a,b,c,d,e,f,g) #endif +/* + * GUC variable to define the minimum size of hash to cosider entry eviction. + * This variable is shared among various cache mechanisms. + */ +int cache_memory_target = 0; + + +/* + * GUC for entry limit. Entries are removed when the number of them goes above + * cache_entry_limit by the ratio specified by cache_entry_limit_prune_ratio + */ +int cache_entry_limit = 0; +double cache_entry_limit_prune_ratio = 0.8; + +/* GUC variable to define the minimum age of entries that will be cosidered to + * be evicted in seconds. This variable is shared among various cache + * mechanisms. + */ +int cache_prune_min_age = 600; + +/* + * Ignorance interval between two success move of a cache entry in LRU list, + * in microseconds. + */ +#define LRU_IGNORANCE_INTERVAL 100000 /* 100ms */ + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Timestamp used for any operation on caches. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -481,6 +510,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) /* delink from linked list */ dlist_delete(&ct->cache_elem); + dlist_delete(&ct->lru_node); /* * Free keys when we're dealing with a negative entry, normal entries just @@ -490,6 +520,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, ct->keys); + cache->cc_tupsize -= ct->size; pfree(ct); --cache->cc_ntup; @@ -841,7 +872,9 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + cp->cc_tupsize = 0; + dlist_init(&cp->cc_lru_list); /* * new cache is initialized as far as we can go for now. print some * debugging information, if appropriate. @@ -858,9 +891,133 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initilize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries can be left alone for several reasons. We remove them if + * they are not accessed for a certain time to prevent catcache from + * bloating. The eviction is performed with the similar algorithm with buffer + * eviction using access counter. Entries that are accessed several times can + * live longer than those that have had no access in the same duration. + */ +#define PRUNE_BY_AGE 0x01 +#define PRUNE_BY_NUMBER 0x02 + +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + size_t hash_size; + int nelems_before = cp->cc_ntup; + int ndelelems = 0; + int action = 0; + dlist_mutable_iter iter; + + if (cache_prune_min_age >= 0) + { + /* prune only if the size of the hash is above the target */ + + hash_size = cp->cc_nbuckets * sizeof(dlist_head); + if (hash_size + cp->cc_tupsize > (Size) cache_memory_target * 1024L) + action |= PRUNE_BY_AGE; + } + + if (cache_entry_limit > 0 && nelems_before >= cache_entry_limit) + { + ndelelems = nelems_before - + (int) (cache_entry_limit * cache_entry_limit_prune_ratio); + + if (ndelelems < 256) + ndelelems = 256; + if (ndelelems > nelems_before) + ndelelems = nelems_before; + + action |= PRUNE_BY_NUMBER; + } + + /* Return immediately if no pruning is wanted */ + if (action == 0) + return false; + + /* Scan over LRU to find entries to remove */ + dlist_foreach_modify(iter, &cp->cc_lru_list) + { + CatCTup *ct = dlist_container(CatCTup, lru_node, iter.cur); + bool remove_this = false; + + /* We don't remove referenced entry */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* check against age */ + if (action & PRUNE_BY_AGE) + { + long entry_age; + int us; + + /* + * Calculate the duration from the time of the last access to the + * "current" time. Since catcacheclock is not advanced within a + * transaction, the entries that are accessed within the current + * transaction won't be pruned. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < cache_prune_min_age) + { + /* no longer have a business with further entries, exit */ + action &= ~PRUNE_BY_AGE; + break; + } + + /* + * Entries that are not accessed after last pruning are removed in + * that seconds, and that has been accessed several times are + * removed after leaving alone for up to three times of the + * duration. We don't try shrink buckets since pruning effectively + * caps catcache expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + remove_this = true; + } + + /* check against entry number */ + if (action & PRUNE_BY_NUMBER) + { + if (nremoved < ndelelems) + remove_this = true; + else + action &= ~PRUNE_BY_NUMBER; /* satisfied */ + } + + /* exit if finished */ + if (action == 0) + break; + + /* do the work */ + if (remove_this) + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, nelems_before); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1274,6 +1431,21 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* Update access information for pruning */ + if (ct->naccess < 2) + ct->naccess++; + + /* + * We don't want too frequent update of LRU. cache_prune_min_age can + * be changed on-session so we need to maintan the LRU regardless of + * cache_prune_min_age. + */ + if (catcacheclock - ct->lastaccess > LRU_IGNORANCE_INTERVAL) + { + ct->lastaccess = catcacheclock; + dlist_move_tail(&cache->cc_lru_list, &ct->lru_node); + } + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1819,11 +1991,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, CatCTup *ct; HeapTuple dtp; MemoryContext oldcxt; + int tupsize = 0; /* negative entries have no tuple associated */ if (ntp) { int i; + int tupsize; Assert(!negative); @@ -1842,13 +2016,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, /* Allocate memory for CatCTup and the cached tuple in one go */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + tupsize = sizeof(CatCTup) + MAXIMUM_ALIGNOF + dtp->t_len; + ct = (CatCTup *) palloc(tupsize); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; ct->tuple.t_data = (HeapTupleHeader) MAXALIGN(((char *) ct) + sizeof(CatCTup)); + ct->size = tupsize; /* copy tuple contents */ memcpy((char *) ct->tuple.t_data, (const char *) dtp->t_data, @@ -1876,8 +2051,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, { Assert(negative); oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup)); - + tupsize = sizeof(CatCTup); + ct = (CatCTup *) palloc(tupsize); /* * Store keys - they'll point into separately allocated memory if not * by-value. @@ -1898,18 +2073,34 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + dlist_push_tail(&cache->cc_lru_list, &ct->lru_node); + ct->size = tupsize; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + cache->cc_tupsize += tupsize; + + /* increase refcount so that this survives pruning */ + ct->refcount++; /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try cleanup by removing + * infrequently used entries to make a room for the new entry. If it + * failed, enlarge the bucket array instead. Quite arbitrarily, we try + * this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + /* we may still want to prune by entry number, check it */ + else if (cache_entry_limit > 0 && cache->cc_ntup > cache_entry_limit) + CatCacheCleanupOldEntries(cache); + + ct->refcount--; return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 8681ada33a..d4df841982 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2204,6 +2205,58 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"cache_memory_target", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum syscache size to keep."), + gettext_noop("Cache is not pruned before exceeding this size."), + GUC_UNIT_KB + }, + &cache_memory_target, + 0, 0, MAX_KILOBYTES, + NULL, NULL, NULL + }, + + { + {"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum unused duration of cache entries before removal."), + gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."), + GUC_UNIT_S + }, + &cache_prune_min_age, + 600, -1, INT_MAX, + NULL, NULL, NULL + }, + + { + {"cache_entry_limit", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &cache_entry_limit, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + + { + {"cache_entry_limit", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &cache_entry_limit, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + + { + {"cache_entry_limit", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &cache_entry_limit, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if @@ -3368,6 +3421,16 @@ static struct config_real ConfigureNamesReal[] = NULL, NULL, NULL }, + { + {"cache_entry_limit_prune_ratio", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the maximum entries of catcache."), + NULL + }, + &cache_entry_limit_prune_ratio, + 0.8, 0.0, 1.0, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index c7f53470df..108d332f2c 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,8 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#cache_memory_target = 0kB # in kB +#cache_prune_min_age = 600s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..3c6842e272 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,9 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + dlist_head cc_lru_list; + int cc_tupsize; /* total amount of catcache tuples */ + int cc_nfreeent; /* # of entries currently not referenced */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,7 +123,10 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ - + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* approx. timestamp of the last usage */ + dlist_node lru_node; /* LRU node */ + int size; /* palloc'ed size off this tuple */ /* * The tuple may also be a member of at most one CatCList. (If a single * catcache is list-searched with varying numbers of keys, we may have to @@ -189,6 +196,30 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int cache_prune_min_age; +extern int cache_memory_target; +extern int cache_entry_limit; +extern double cache_entry_limit_prune_ratio; + +/* to use as access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* + * SetCatCacheClock - set timestamp for catcache access record + */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + +static inline TimestampTz +GetCatCacheClock(void) +{ + return catcacheclock; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Thu_Feb_07_21_18_45_2019_504)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v13-0003-Syscache-usage-tracking-feature.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 1/4] Remove entries that haven't been used for a certain time @ 2018-10-16 04:04 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2018-10-16 04:04 UTC (permalink / raw) Catcache entries can be left alone for several reasons. It is not desirable that they eat up memory. With this patch, This adds consideration of removal of entries that haven't been used for a certain time before enlarging the hash array. --- doc/src/sgml/config.sgml | 38 ++++++ src/backend/access/transam/xact.c | 5 + src/backend/utils/cache/catcache.c | 168 ++++++++++++++++++++++++-- src/backend/utils/misc/guc.c | 23 ++++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/utils/catcache.h | 28 ++++- 6 files changed, 256 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 9b7a7388d5..d0d2374944 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1662,6 +1662,44 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-syscache-memory-target" xreflabel="syscache_memory_target"> + <term><varname>syscache_memory_target</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_memory_target</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum amount of memory to which syscache is expanded + without pruning. The value defaults to 0, indicating that pruning is + always considered. After exceeding this size, syscache pruning is + considered according to + <xref linkend="guc-syscache-prune-min-age"/>. If you need to keep + certain amount of syscache entries with intermittent usage, try + increase this setting. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-syscache-prune-min-age" xreflabel="syscache_prune_min_age"> + <term><varname>syscache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>syscache_prune_min_age</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + syscache entry is considered to be removed. -1 indicates that syscache + pruning is disabled at all. The value defaults to 600 seconds + (<literal>10 minutes</literal>). The syscache entries that are not + used for the duration can be removed to prevent syscache bloat. This + behavior is suppressed until the size of syscache exceeds + <xref linkend="guc-syscache-memory-target"/>. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 92bda87804..ddc433c59e 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -734,7 +734,12 @@ void SetCurrentStatementStartTimestamp(void) { if (!IsParallelWorker()) + { stmtStartTimestamp = GetCurrentTimestamp(); + + /* Set this timestamp as aproximated current time */ + SetCatCacheClock(stmtStartTimestamp); + } else Assert(stmtStartTimestamp != 0); } diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 258a1d64cc..5106ed896a 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -71,9 +71,24 @@ #define CACHE6_elog(a,b,c,d,e,f,g) #endif +/* + * GUC variable to define the minimum size of hash to cosider entry eviction. + * This variable is shared among various cache mechanisms. + */ +int cache_memory_target = 0; + +/* GUC variable to define the minimum age of entries that will be cosidered to + * be evicted in seconds. This variable is shared among various cache + * mechanisms. + */ +int cache_prune_min_age = 600; + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Timestamp used for any operation on caches. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -490,6 +505,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) CatCacheFreeKeys(cache->cc_tupdesc, cache->cc_nkeys, cache->cc_keyno, ct->keys); + cache->cc_tupsize -= ct->size; pfree(ct); --cache->cc_ntup; @@ -841,6 +857,7 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + cp->cc_tupsize = 0; /* * new cache is initialized as far as we can go for now. print some @@ -858,9 +875,127 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initilize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries can be left alone for several reasons. We remove them if + * they are not accessed for a certain time to prevent catcache from + * bloating. The eviction is performed with the similar algorithm with buffer + * eviction using access counter. Entries that are accessed several times can + * live longer than those that have had no access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int i; + int nremoved = 0; + size_t hash_size; +#ifdef CATCACHE_STATS + /* These variables are only for debugging purpose */ + int ntotal = 0; + /* + * nth element in nentries stores the number of cache entries that have + * lived unaccessed for corresponding multiple in ageclass of + * cache_prune_min_age. The index of nremoved_entry is the value of the + * clock-sweep counter, which takes from 0 up to 2. + */ + double ageclass[] = {0.05, 0.1, 1.0, 2.0, 3.0, 0.0}; + int nentries[] = {0, 0, 0, 0, 0, 0}; + int nremoved_entry[3] = {0, 0, 0}; + int j; +#endif + + /* Return immediately if no pruning is wanted */ + if (cache_prune_min_age < 0) + return false; + + /* + * Return without pruning if the size of the hash is below the target. + */ + hash_size = cp->cc_nbuckets * sizeof(dlist_head); + if (hash_size + cp->cc_tupsize < (Size) cache_memory_target * 1024L) + return false; + + /* Search the whole hash for entries to remove */ + for (i = 0; i < cp->cc_nbuckets; i++) + { + dlist_mutable_iter iter; + + dlist_foreach_modify(iter, &cp->cc_bucket[i]) + { + CatCTup *ct = dlist_container(CatCTup, cache_elem, iter.cur); + long entry_age; + int us; + + + /* + * Calculate the duration from the time of the last access to the + * "current" time. Since catcacheclock is not advanced within a + * transaction, the entries that are accessed within the current + * transaction won't be pruned. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + +#ifdef CATCACHE_STATS + /* count catcache entries for each age class */ + ntotal++; + for (j = 0 ; + ageclass[j] != 0.0 && + entry_age > cache_prune_min_age * ageclass[j] ; + j++); + if (ageclass[j] == 0.0) j--; + nentries[j]++; +#endif + + /* + * Try to remove entries older than cache_prune_min_age seconds. + * Entries that are not accessed after last pruning are removed in + * that seconds, and that has been accessed several times are + * removed after leaving alone for up to three times of the + * duration. We don't try shrink buckets since pruning effectively + * caps catcache expansion in the long term. + */ + if (entry_age > cache_prune_min_age) + { +#ifdef CATCACHE_STATS + Assert (ct->naccess >= 0 && ct->naccess <= 2); + nremoved_entry[ct->naccess]++; +#endif + if (ct->naccess > 0) + ct->naccess--; + else if (ct->refcount == 0 && + (!ct->c_list || ct->c_list->refcount == 0)) + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + } + } + +#ifdef CATCACHE_STATS + ereport(DEBUG1, + (errmsg ("removed %d/%d, age(-%.0fs:%d, -%.0fs:%d, *-%.0fs:%d, -%.0fs:%d, -%.0fs:%d) naccessed(0:%d, 1:%d, 2:%d)", + nremoved, ntotal, + ageclass[0] * cache_prune_min_age, nentries[0], + ageclass[1] * cache_prune_min_age, nentries[1], + ageclass[2] * cache_prune_min_age, nentries[2], + ageclass[3] * cache_prune_min_age, nentries[3], + ageclass[4] * cache_prune_min_age, nentries[4], + nremoved_entry[0], nremoved_entry[1], nremoved_entry[2]), + errhidestmt(true))); +#endif + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1274,6 +1409,11 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* Update access information for pruning */ + if (ct->naccess < 2) + ct->naccess++; + ct->lastaccess = catcacheclock; + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1819,11 +1959,13 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, CatCTup *ct; HeapTuple dtp; MemoryContext oldcxt; + int tupsize = 0; /* negative entries have no tuple associated */ if (ntp) { int i; + int tupsize; Assert(!negative); @@ -1842,13 +1984,14 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, /* Allocate memory for CatCTup and the cached tuple in one go */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + tupsize = sizeof(CatCTup) + MAXIMUM_ALIGNOF + dtp->t_len; + ct = (CatCTup *) palloc(tupsize); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; ct->tuple.t_data = (HeapTupleHeader) MAXALIGN(((char *) ct) + sizeof(CatCTup)); + ct->size = tupsize; /* copy tuple contents */ memcpy((char *) ct->tuple.t_data, (const char *) dtp->t_data, @@ -1876,8 +2019,8 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, { Assert(negative); oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup)); - + tupsize = sizeof(CatCTup); + ct = (CatCTup *) palloc(tupsize); /* * Store keys - they'll point into separately allocated memory if not * by-value. @@ -1898,19 +2041,30 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + ct->size = tupsize; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + cache->cc_tupsize += tupsize; + /* increase refcount so that this survives pruning */ + ct->refcount++; /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try cleanup by removing + * infrequently used entries to make a room for the new entry. If it + * failed, enlarge the bucket array instead. Quite arbitrarily, we try + * this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + ct->refcount--; + return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 8681ada33a..06c589f725 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2204,6 +2205,28 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"cache_memory_target", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum syscache size to keep."), + gettext_noop("Cache is not pruned before exceeding this size."), + GUC_UNIT_KB + }, + &cache_memory_target, + 0, 0, MAX_KILOBYTES, + NULL, NULL, NULL + }, + + { + {"cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("Sets the minimum unused duration of cache entries before removal."), + gettext_noop("Cache entries that live unused for longer than this seconds are considered to be removed."), + GUC_UNIT_S + }, + &cache_prune_min_age, + 600, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index c7f53470df..108d332f2c 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,8 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#cache_memory_target = 0kB # in kB +#cache_prune_min_age = 600s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..5d24809900 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,7 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + int cc_tupsize; /* total amount of catcache tuples */ /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,7 +121,9 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ - + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* approx. timestamp of the last usage */ + int size; /* palloc'ed size off this tuple */ /* * The tuple may also be a member of at most one CatCList. (If a single * catcache is list-searched with varying numbers of keys, we may have to @@ -189,6 +193,28 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int cache_prune_min_age; +extern int cache_memory_target; + +/* to use as access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* + * SetCatCacheClock - set timestamp for catcache access record + */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + +static inline TimestampTz +GetCatCacheClock(void) +{ + return catcacheclock; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Wed_Feb_06_17_37_04_2019_764)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v11-0002-Syscache-usage-tracking-feature.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH] Remove entries that haven't been used for a certain time @ 2019-03-01 04:32 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw) Catcache entries happen to be left alone for several reasons. It is not desirable that such useless entries eat up memory. Catcache pruning feature removes entries that haven't been accessed for a certain time before enlarging hash array. --- doc/src/sgml/config.sgml | 19 ++++ src/backend/tcop/postgres.c | 2 + src/backend/utils/cache/catcache.c | 121 +++++++++++++++++++++++++- src/backend/utils/misc/guc.c | 12 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/utils/catcache.h | 17 ++++ 6 files changed, 169 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index d383de2512..4231235447 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1677,6 +1677,25 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + system catalog cache entry is removed. -1 indicates that this feature + is disabled at all. The value defaults to 300 seconds (<literal>5 + minutes</literal>). The entries that are not used for the duration + can be removed to prevent catalog cache from bloating with useless + entries. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index f9ce3d8f22..acab473d34 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2575,6 +2576,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index d05930bc4c..c4582fe5a3 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -38,6 +38,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -60,9 +61,18 @@ #define CACHE_elog(...) #endif +/* + * GUC variable to define the minimum age of entries that will be considered + * to be evicted in seconds. -1 to disable the feature. + */ +int catalog_cache_prune_min_age = 300; + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock for the last accessed time of a catcache entry. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -850,9 +860,99 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries happen to be left unused for a long time for several + * reasons. Remove such entries to prevent catcache from bloating. It is based + * on the similar algorithm with buffer eviction. Entries that are accessed + * several times in a certain period live longer than those that have had less + * access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + int i; + long oldest_ts = catcacheclock; + long age; + int us; + + /* Return immediately if disabled */ + if (catalog_cache_prune_min_age == 0) + return false; + + /* Don't scan the hash when we know we don't have prunable entries */ + TimestampDifference(cp->cc_oldest_ts, catcacheclock, &age, &us); + if (age < catalog_cache_prune_min_age) + return false; + + /* Scan over the whole hash to find entries to remove */ + for (i = 0 ; i < cp->cc_nbuckets ; i++) + { + dlist_mutable_iter iter; + + dlist_foreach_modify(iter, &cp->cc_bucket[i]) + { + CatCTup *ct = dlist_container(CatCTup, cache_elem, iter.cur); + + /* Don't remove referenced entries */ + if (ct->refcount == 0 && + (ct->c_list == NULL || ct->c_list->refcount == 0)) + { + /* + * Calculate the duration from the time from the last access + * to the "current" time. catcacheclock is updated + * per-statement basis and additionaly udpated periodically + * during a long running query. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &age, &us); + + if (age >= catalog_cache_prune_min_age) + { + /* + * Entries that are not accessed after the last pruning + * are removed in that seconds, and their lives are + * prolonged according to how many times they are accessed + * up to three times of the duration. We don't try shrink + * buckets since pruning effectively caps catcache + * expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + + /* don't update oldest_ts by removed entry */ + continue; + } + } + } + + /* update oldest timestamp if the entry remains alive */ + if (ct->lastaccess < oldest_ts) + oldest_ts = ct->lastaccess; + } + } + + cp->cc_oldest_ts = oldest_ts; + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1264,6 +1364,12 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* prolong life of this entry */ + if (ct->naccess < 2) + ct->naccess++; + + ct->lastaccess = catcacheclock; + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1888,19 +1994,28 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + /* increase refcount so that the new entry survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try removing infrequently used + * entries to make a room for the new entry. If failed, enlarge the bucket + * array instead. Quite arbitrarily, we try this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + ct->refcount--; + return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index aa564d153a..e624c74bf9 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -82,6 +82,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2202,6 +2203,17 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."), + gettext_noop("The value of -1 turns off pruning."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index cccb5f145a..fa117f0573 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,7 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..2f697d5ca4 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,7 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + long cc_oldest_ts; /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,6 +121,8 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* timestamp of the last usage */ /* * The tuple may also be a member of at most one CatCList. (If a single @@ -189,6 +193,19 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; + +/* source clock for access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* SetCatCacheClock - set catcache timestamp source clodk */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Fri_Mar_29_17_24_40_2019_805)-- Content-Type: Application/Octet-Stream Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="test_script.tar.gz" H4sIAFPSnVwAA+1b63Paxhb3V+uvOAb5WtQG9AIaO7h1J2mvZ1y74yb3fkhTRkgLqBGSol01ztjc v/2e3ZVAflCblIhJo+MZHrtnz/t3dpElRigbUDfxY9be+kykI/V6Hf5u9Dp68T2nLcOy7Z7VMQzd 2NINq9vTt6DzuQwqUkqZkwBsTaLEH6fMX8b32PwXSqyQ/zEJB2wYtOJgvTp4grtde2n+bduQ+dc7 lmXhuNHp4hDo6zXjYfrK81/fgXZKk/bQD9sxSQJFdaMg8CmDPtRqR8ooSojjTkD1QdNbLQPT1YBr ZXvO1uoDjRM/ZCOtdgDurt7xAL/hZ9VvHCmzokCaDilLtHzkAEzkUORqqHlJFAN1J2TqgD8CcoUs FHiBgutQ1/HI0W8hmpTzu2gZI/kKzifn75hsPnv2TJh8Zx1zhgERy1psV7c9mNvV4HK4/dz8TSfo M1MR/0katuhk/ToewX9XtzL8W6ah9wT+dcus8F8G1XcE9IcOnShnFxe/nJ3++qqvapS8BwNUo6H8 cHp+eXHxqt+eRFPSzqPAFykvTl6dvDi9vDvlOcwRL4MPUfJuEJlcxs8nv756edlXM3HteEzfB0WG s8vXd2ddhOlV/O3A4PM/vj47W8Zg5gw/X7xYxmMpZyc/vDz798uTF33x6VBR3vlB4AQBxBFl44RQ hQaExGAp6txi2RjHA5cF0GzGY+5XX808B147TCHuJAL1ei5/NnUoI0nTGOi8GYGPDRHUPLpH4EWg MX9KgDdccFyXUMo3XgN3XriBGM2emwTH0PbIn+0wDYIGmMf/Mvj6kCxVaq6o1FyHUmtFpci/itqn pyOKFUWwYzl9UupwXcl5kxqN1TQO/r7OcgtFalzNS3MNXq5cmWvQuJqX1mpePrG4ORDUrCt+EhB+ RMUlIyFTWS4UhNJysZCpLBcMQmm5aMhUfnY4PKXGi3jAU8KnQ2IaeZuAhVS7AWhwxRuAh1S7AYhw xRuAiVRbClSeUP6Ilk3/MKmoFCr+/r/bOdal47Hrf1bPlL//Datrmhb+/kduvfr9Xwbdu/4nL69B jRJ+2Y05QYRtwnEnZBAnaUgGUz8cOGMCLAJdXm/bwCXDxy7xURIQl8E3MEqiaeEa31d0Ye+JtAT/ a/0fwKPX//P//ximoRv8+l/X6tkV/sug+/ivP70B7Fm6TveqLvAF08P4Nze3/1u23P+tCv9l0D9i /+88Bfm80irk36Ul+N/M/m/2Oj27w/Fvm90K/2XQP2j/r7rAJ9DD+Jf/n1uXjkf3f3u+/2MLyPb/ ToX/Mug+/svH8ULHH1JHQ0EFy073Kxzvt2cVwh+hZfgv8/xvm/bi/G8b1fW/EunLPP9XHWNdtAT/ 5Z7/rfn1P9voCPzbRnX/Xyn0d/C/Z2zy9F/1gHVQEf9xEvEGUPb9/7bVzZ//4I+A9Pj9vz2r2v9L ofrObfinlMAZguzw8DXzA3j/oU3TaRvh5vkEwTOJ0sBD5BLXH30E7A0w8gMCoTMlNX7TvqbWTy5/ +g/s9EFHkKp+GKdsIHj6oPKpN/pbHB/5JPACEuLot/g1dP4kCe8qfejwZhJSZxoHhOL3poHzmCPG J/Uj5Xs38sSEhvKjGEWcnh/A3vM9Dt6FtpsbEBaPHPzm8W4leNHiFrti2KE+TDib9vz0/Fg0CHcS TWMuczskV0z40v5dbfNOsy0cy2zgjomGIjnkrcRa65sG8orxbVVgKnCGJOBOo/04KIyRY7GTYJCl YYdQ4K5xRqmsIGLnf9D+nStoam/05rO3DW2QfWh8J+zjKnlQ+pkqsbivmosvA+ygPGIeGfkhRkO1 4Tv+cgg1vSYlfC9uMJFR5cvyiAuRM+yJQAJ6Ow5GMQ7YdYPf6L4+Fba13u436DwgcUonmlBwwG8p FxJzcTvoG9ZcckM/0ka+oA4JcaMEC02UAYTpdEgSiEbAJhg5P8Ftgful5OGa18vzPDvoQqGI1Lpw b1+6k2UjjHL5oygNvax+C6KkqfdUoLi8XnNd29mm+d+Ty/PT858OcSuj6Wjkuz7B4Xylxst9Lqgh Nk6+eKYo4hVf8iBLe3cK7NDMA56Zr7ZuFQ/msjVfuM95W9K02ly/cBOGKStIJVeIZUa8Vm7NrFgN NEoYXKsOOn0M6nAGYuJIkUlizjsiMpKjN08QvykcU8Q1UfmYTSfLIb2fsuP74VTlrj+VRfMG9/FF h8AwvG1Ae7FIWs3riWTL0bIIczvlt/ZLLQfwR8prhhucLXtAERdbjOAiHmpCaBqw65p6zT2bDeQ7 x9WsNrsW8Jvx5DIZG+4hdZ3ASbRxQmK4BnUA5D0IRsAwiibWKHSTHCVi4kAyFgzYvovRWyDVBWNC vAiKXUk2h5k80LhBhAE6PceldaUOsmClW/JRKBzGcTxjpdMQmM9wJD8z4d8VprAFtRt5tssc0XI/ +MmOxrKf4xJN0xYdvgn4OmYTTfrEg2w2uGWqyw+LQna2uCXltiAb1rLxLBliwfwweAA6xilXw+cz c1U3M3SWn2XlEbUOlGDvdViUzD1rPsGzW7wLv3DNfu2+Dlnvw8j7KMUJJGgCSO/IRwq7Mua3JO/a FG746VM8xCYOsQ/YIXEjhkPEv3z0opZvOlmPxBrMalUIy2tz3nNJ3mHlcjzih2HE+J2FqHQK/Izv h2N5LgYvjQOfP0ADYRQ28zmpaQ7kh9QhqAt9eG5ZgDozvX9h520wP7h8DoxZ4WQPu92WNZKBzG2c KdvF/Hw95/qKKqqooooqqqiiiiqqqKKKKqqooooq4vR/BXY1+wBQAAA= ----Next_Part(Fri_Mar_29_17_24_40_2019_805)---- ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH] Remove entries that haven't been used for a certain time @ 2019-03-01 04:32 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw) Catcache entries happen to be left alone for several reasons. It is not desirable that such useless entries eat up memory. Catcache pruning feature removes entries that haven't been accessed for a certain time before enlarging hash array. --- doc/src/sgml/config.sgml | 19 +++++ src/backend/tcop/postgres.c | 2 + src/backend/utils/cache/catcache.c | 105 +++++++++++++++++++++++++- src/backend/utils/misc/guc.c | 12 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/utils/catcache.h | 16 ++++ 6 files changed, 152 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index d383de2512..4231235447 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1677,6 +1677,25 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + system catalog cache entry is removed. -1 indicates that this feature + is disabled at all. The value defaults to 300 seconds (<literal>5 + minutes</literal>). The entries that are not used for the duration + can be removed to prevent catalog cache from bloating with useless + entries. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index f9ce3d8f22..acab473d34 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2575,6 +2576,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index d05930bc4c..52586bd415 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -38,6 +38,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -60,9 +61,18 @@ #define CACHE_elog(...) #endif +/* + * GUC variable to define the minimum age of entries that will be considered + * to be evicted in seconds. -1 to disable the feature. + */ +int catalog_cache_prune_min_age = 300; + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock for the last accessed time of a catcache entry. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -850,9 +860,83 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries happen to be left unused for a long time for several + * reasons. Remove such entries to prevent catcache from bloating. It is based + * on the similar algorithm with buffer eviction. Entries that are accessed + * several times in a certain period live longer than those that have had less + * access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + int i; + + /* Return immediately if disabled */ + if (catalog_cache_prune_min_age == 0) + return false; + + /* Scan over the whole hash to find entries to remove */ + for (i = 0 ; i < cp->cc_nbuckets ; i++) + { + dlist_mutable_iter iter; + + dlist_foreach_modify(iter, &cp->cc_bucket[i]) + { + CatCTup *ct = dlist_container(CatCTup, cache_elem, iter.cur); + long entry_age; + int us; + + /* Don't remove referenced entries */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* + * Calculate the duration from the time from the last access to + * the "current" time. catcacheclock is updated per-statement + * basis and additionaly udpated periodically during a long + * running query. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < catalog_cache_prune_min_age) + continue; + + /* + * Entries that are not accessed after the last pruning are + * removed in that seconds, and their lives are prolonged + * according to how many times they are accessed up to three times + * of the duration. We don't try shrink buckets since pruning + * effectively caps catcache expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + } + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1264,6 +1348,12 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* prolong life of this entry */ + if (ct->naccess < 2) + ct->naccess++; + + ct->lastaccess = catcacheclock; + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1888,19 +1978,28 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + /* increase refcount so that the new entry survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try removing infrequently used + * entries to make a room for the new entry. If failed, enlarge the bucket + * array instead. Quite arbitrarily, we try this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + ct->refcount--; + return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index aa564d153a..e624c74bf9 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -82,6 +82,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2202,6 +2203,17 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."), + gettext_noop("The value of -1 turns off pruning."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index cccb5f145a..fa117f0573 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,7 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..2134839ecf 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -119,6 +120,8 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* timestamp of the last usage */ /* * The tuple may also be a member of at most one CatCList. (If a single @@ -189,6 +192,19 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; + +/* source clock for access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* SetCatCacheClock - set catcache timestamp source clodk */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Fri_Mar_29_17_24_40_2019_805)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="FullScan-mod-0001-Remove-entries-that-haven-t-been-used-for-a-certain-.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 2/2] Remove entries that haven't been used for a certain time @ 2019-03-01 04:32 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw) Catcache entries happen to be left alone for several reasons. It is not desirable that such useless entries eat up memory. Catcache pruning feature removes entries that haven't been accessed for a certain time before enlarging hash array. --- doc/src/sgml/config.sgml | 19 ++++ src/backend/tcop/postgres.c | 2 + src/backend/utils/cache/catcache.c | 122 +++++++++++++++++++++++++- src/backend/utils/misc/guc.c | 12 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/utils/catcache.h | 18 ++++ 6 files changed, 171 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index d383de2512..4231235447 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1677,6 +1677,25 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + system catalog cache entry is removed. -1 indicates that this feature + is disabled at all. The value defaults to 300 seconds (<literal>5 + minutes</literal>). The entries that are not used for the duration + can be removed to prevent catalog cache from bloating with useless + entries. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index f9ce3d8f22..acab473d34 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2575,6 +2576,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index d05930bc4c..c8ee0c98fb 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -38,6 +38,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -60,9 +61,24 @@ #define CACHE_elog(...) #endif +/* + * GUC variable to define the minimum age of entries that will be considered + * to be evicted in seconds. -1 to disable the feature. + */ +int catalog_cache_prune_min_age = 300; + +/* + * Minimum interval between two successive moves of a cache entry in LRU list, + * in microseconds. + */ +#define MIN_LRU_UPDATE_INTERVAL 100000 /* 100ms */ + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock for the last accessed time of a catcache entry. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -473,6 +489,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) /* delink from linked list */ dlist_delete(&ct->cache_elem); + dlist_delete(&ct->lru_node); /* * Free keys when we're dealing with a negative entry, normal entries just @@ -833,6 +850,7 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + dlist_init(&cp->cc_lru_list); /* * new cache is initialized as far as we can go for now. print some @@ -850,9 +868,83 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries happen to be left unused for a long time for several + * reasons. Remove such entries to prevent catcache from bloating. It is based + * on the similar algorithm with buffer eviction. Entries that are accessed + * several times in a certain period live longer than those that have had less + * access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + dlist_mutable_iter iter; + + /* Return immediately if disabled */ + if (catalog_cache_prune_min_age == 0) + return false; + + /* Scan over LRU to find entries to remove */ + dlist_foreach_modify(iter, &cp->cc_lru_list) + { + CatCTup *ct = dlist_container(CatCTup, lru_node, iter.cur); + long entry_age; + int us; + + /* Don't remove referenced entries */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* + * Calculate the duration from the time from the last access to + * the "current" time. catcacheclock is updated per-statement + * basis. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < catalog_cache_prune_min_age) + { + /* + * We don't have older entries, exit. At least one removal + * prevents rehashing this time. + */ + break; + } + + /* + * Entries that are not accessed after the last pruning are removed in + * that seconds, and their lives are prolonged according to how many + * times they are accessed up to three times of the duration. We don't + * try shrink buckets since pruning effectively caps catcache + * expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1264,6 +1356,20 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* prolong life of this entry */ + if (ct->naccess < 2) + ct->naccess++; + + /* + * Don't update LRU too frequently. We need to maintain the LRU even + * if pruning is inactive since it can be turned on on-session. + */ + if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL) + { + ct->lastaccess = catcacheclock; + dlist_move_tail(&cache->cc_lru_list, &ct->lru_node); + } + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1888,19 +1994,29 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + dlist_push_tail(&cache->cc_lru_list, &ct->lru_node); dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + /* increase refcount so that the new entry survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try removing infrequently used + * entries to make a room for the new entry. If failed, enlarge the bucket + * array instead. Quite arbitrarily, we try this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + ct->refcount--; + return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index aa564d153a..e624c74bf9 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -82,6 +82,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2202,6 +2203,17 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."), + gettext_noop("The value of -1 turns off pruning."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index cccb5f145a..fa117f0573 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,7 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..a21c53644a 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,7 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + dlist_head cc_lru_list; /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,6 +121,9 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* timestamp of the last usage */ + dlist_node lru_node; /* LRU node */ /* * The tuple may also be a member of at most one CatCList. (If a single @@ -189,6 +194,19 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; + +/* source clock for access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* SetCatCacheClock - set catcache timestamp source clodk */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Fri_Mar_29_17_24_40_2019_805)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="FullScan-0001-Remove-entries-that-haven-t-been-used-for-a-certain-.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH] Remove entries that haven't been used for a certain time @ 2019-03-01 04:32 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw) Catcache entries happen to be left alone for several reasons. It is not desirable that such useless entries eat up memory. Catcache pruning feature removes entries that haven't been accessed for a certain time before enlarging hash array. --- doc/src/sgml/config.sgml | 19 +++++ src/backend/tcop/postgres.c | 2 + src/backend/utils/cache/catcache.c | 103 +++++++++++++++++++++++++- src/backend/utils/misc/guc.c | 12 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/utils/catcache.h | 16 ++++ 6 files changed, 150 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index bc1d0f7bfa..819b252029 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1677,6 +1677,25 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + system catalog cache entry is removed. -1 indicates that this feature + is disabled at all. The value defaults to 300 seconds (<literal>5 + minutes</literal>). The entries that are not used for the duration + can be removed to prevent catalog cache from bloating with useless + entries. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 44a59e1d4f..a0efac86bc 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2577,6 +2578,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index d05930bc4c..03c2d8524c 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -38,6 +38,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -60,9 +61,18 @@ #define CACHE_elog(...) #endif +/* + * GUC variable to define the minimum age of entries that will be considered + * to be evicted in seconds. -1 to disable the feature. + */ +int catalog_cache_prune_min_age = 300; + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock for the last accessed time of a catcache entry. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -850,9 +860,83 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries happen to be left unused for a long time for several + * reasons. Remove such entries to prevent catcache from bloating. It is based + * on the similar algorithm with buffer eviction. Entries that are accessed + * several times in a certain period live longer than those that have had less + * access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + int i; + + /* Return immediately if disabled */ + if (catalog_cache_prune_min_age == 0) + return false; + + /* Scan over the whole hash to find entries to remove */ + for (i = 0 ; i < cp->cc_nbuckets ; i++) + { + dlist_mutable_iter iter; + + dlist_foreach_modify(iter, &cp->cc_bucket[i]) + { + CatCTup *ct = dlist_container(CatCTup, cache_elem, iter.cur); + long entry_age; + int us; + + /* Don't remove referenced entries */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* + * Calculate the duration from the time from the last access to + * the "current" time. catcacheclock is updated per-statement + * basis and additionaly udpated periodically during a long + * running query. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < catalog_cache_prune_min_age) + continue; + + /* + * Entries that are not accessed after the last pruning are + * removed in that seconds, and their lives are prolonged + * according to how many times they are accessed up to three times + * of the duration. We don't try shrink buckets since pruning + * effectively caps catcache expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + } + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1263,6 +1347,10 @@ SearchCatCacheInternal(CatCache *cache, * near the front of the hashbucket's list.) */ dlist_move_head(bucket, &ct->cache_elem); + if (ct->naccess < 2) + ct->naccess++; + + ct->lastaccess = catcacheclock; /* * If it's a positive entry, bump its refcount and return it. If it's @@ -1888,19 +1976,28 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + /* increase refcount so that the new entry survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try removing infrequently used + * entries to make a room for the new entry. If failed, enlarge the bucket + * array instead. Quite arbitrarily, we try this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + ct->refcount--; + return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 1766e46037..e671d4428e 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -82,6 +82,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2249,6 +2250,17 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."), + gettext_noop("The value of -1 turns off pruning."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index bbbeb4bb15..d88ec57382 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,7 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..2134839ecf 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -119,6 +120,8 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* timestamp of the last usage */ /* * The tuple may also be a member of at most one CatCList. (If a single @@ -189,6 +192,19 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; + +/* source clock for access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* SetCatCacheClock - set catcache timestamp source clodk */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Fri_Apr_05_09_44_07_2019_159)---- ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 2/6] Remove entries that haven't been used for a certain time @ 2019-03-01 04:32 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw) Catcache entries happen to be left alone for several reasons. It is not desirable that such useless entries eat up memory. Catcache pruning feature removes entries that haven't been accessed for a certain time before enlarging hash array. --- doc/src/sgml/config.sgml | 19 ++++ src/backend/tcop/postgres.c | 2 + src/backend/utils/cache/catcache.c | 122 +++++++++++++++++++++++++- src/backend/utils/misc/guc.c | 12 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/utils/catcache.h | 18 ++++ 6 files changed, 171 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 6d42b7afe7..737a156bb4 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1661,6 +1661,25 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + system catalog cache entry is removed. -1 indicates that this feature + is disabled at all. The value defaults to 300 seconds (<literal>5 + minutes</literal>). The entries that are not used for the duration + can be removed to prevent catalog cache from bloating with useless + entries. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 8b4d94c9a1..02b9ef98aa 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2584,6 +2585,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 78dd5714fa..4386957497 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -39,6 +39,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -61,9 +62,24 @@ #define CACHE_elog(...) #endif +/* + * GUC variable to define the minimum age of entries that will be considered + * to be evicted in seconds. -1 to disable the feature. + */ +int catalog_cache_prune_min_age = 300; + +/* + * Minimum interval between two successive moves of a cache entry in LRU list, + * in microseconds. + */ +#define MIN_LRU_UPDATE_INTERVAL 100000 /* 100ms */ + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock for the last accessed time of a catcache entry. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -469,6 +485,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) /* delink from linked list */ dlist_delete(&ct->cache_elem); + dlist_delete(&ct->lru_node); /* * Free keys when we're dealing with a negative entry, normal entries just @@ -829,6 +846,7 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + dlist_init(&cp->cc_lru_list); /* * new cache is initialized as far as we can go for now. print some @@ -846,9 +864,83 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries happen to be left unused for a long time for several + * reasons. Remove such entries to prevent catcache from bloating. It is based + * on the similar algorithm with buffer eviction. Entries that are accessed + * several times in a certain period live longer than those that have had less + * access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + dlist_mutable_iter iter; + + /* Return immediately if disabled */ + if (catalog_cache_prune_min_age == 0) + return false; + + /* Scan over LRU to find entries to remove */ + dlist_foreach_modify(iter, &cp->cc_lru_list) + { + CatCTup *ct = dlist_container(CatCTup, lru_node, iter.cur); + long entry_age; + int us; + + /* Don't remove referenced entries */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* + * Calculate the duration from the time from the last access to + * the "current" time. catcacheclock is updated per-statement + * basis. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < catalog_cache_prune_min_age) + { + /* + * We don't have older entries, exit. At least one removal + * prevents rehashing this time. + */ + break; + } + + /* + * Entries that are not accessed after the last pruning are removed in + * that seconds, and their lives are prolonged according to how many + * times they are accessed up to three times of the duration. We don't + * try shrink buckets since pruning effectively caps catcache + * expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1260,6 +1352,20 @@ SearchCatCacheInternal(CatCache *cache, */ dlist_move_head(bucket, &ct->cache_elem); + /* prolong life of this entry */ + if (ct->naccess < 2) + ct->naccess++; + + /* + * Don't update LRU too frequently. We need to maintain the LRU even + * if pruning is inactive since it can be turned on on-session. + */ + if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL) + { + ct->lastaccess = catcacheclock; + dlist_move_tail(&cache->cc_lru_list, &ct->lru_node); + } + /* * If it's a positive entry, bump its refcount and return it. If it's * negative, we can report failure to the caller. @@ -1884,19 +1990,29 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + dlist_push_tail(&cache->cc_lru_list, &ct->lru_node); dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + /* increase refcount so that the new entry survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try removing infrequently used + * entries to make a room for the new entry. If failed, enlarge the bucket + * array instead. Quite arbitrarily, we try this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + ct->refcount--; + return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 156d147c85..3acc86cd07 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -81,6 +81,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2205,6 +2206,17 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."), + gettext_noop("The value of -1 turns off pruning."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index bd6ea65d0c..e9e3acc903 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,7 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..a21c53644a 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,7 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + dlist_head cc_lru_list; /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,6 +121,9 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* timestamp of the last usage */ + dlist_node lru_node; /* LRU node */ /* * The tuple may also be a member of at most one CatCList. (If a single @@ -189,6 +194,19 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; + +/* source clock for access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* SetCatCacheClock - set catcache timestamp source clodk */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Fri_Mar_01_17_32_45_2019_112)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v18-0003-Asynchronous-update-of-catcache-clock.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH 1/2] Remove entries that haven't been used for a certain time @ 2019-03-01 04:32 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Kyotaro Horiguchi @ 2019-03-01 04:32 UTC (permalink / raw) Catcache entries happen to be left alone for several reasons. It is not desirable that such useless entries eat up memory. Catcache pruning feature removes entries that haven't been accessed for a certain time before enlarging hash array. --- doc/src/sgml/config.sgml | 19 ++++ src/backend/tcop/postgres.c | 2 + src/backend/utils/cache/catcache.c | 124 +++++++++++++++++++++++++- src/backend/utils/misc/guc.c | 12 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/utils/catcache.h | 18 ++++ 6 files changed, 172 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index bc1d0f7bfa..819b252029 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1677,6 +1677,25 @@ include_dir 'conf.d' </listitem> </varlistentry> + <varlistentry id="guc-catalog-cache-prune-min-age" xreflabel="catalog_cache_prune_min_age"> + <term><varname>catalog_cache_prune_min_age</varname> (<type>integer</type>) + <indexterm> + <primary><varname>catalog_cache_prune_min_age</varname> configuration + parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the minimum amount of unused time in seconds at which a + system catalog cache entry is removed. -1 indicates that this feature + is disabled at all. The value defaults to 300 seconds (<literal>5 + minutes</literal>). The entries that are not used for the duration + can be removed to prevent catalog cache from bloating with useless + entries. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth"> <term><varname>max_stack_depth</varname> (<type>integer</type>) <indexterm> diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 44a59e1d4f..a0efac86bc 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -71,6 +71,7 @@ #include "tcop/pquery.h" #include "tcop/tcopprot.h" #include "tcop/utility.h" +#include "utils/catcache.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -2577,6 +2578,7 @@ start_xact_command(void) * not desired, the timeout has to be disabled explicitly. */ enable_statement_timeout(); + SetCatCacheClock(GetCurrentStatementStartTimestamp()); } static void diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index d05930bc4c..e85f2b038c 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -38,6 +38,7 @@ #include "utils/rel.h" #include "utils/resowner_private.h" #include "utils/syscache.h" +#include "utils/timeout.h" /* #define CACHEDEBUG */ /* turns DEBUG elogs on */ @@ -60,9 +61,24 @@ #define CACHE_elog(...) #endif +/* + * GUC variable to define the minimum age of entries that will be considered + * to be evicted in seconds. -1 to disable the feature. + */ +int catalog_cache_prune_min_age = 300; + +/* + * Minimum interval between two successive moves of a cache entry in LRU list, + * in microseconds. + */ +#define MIN_LRU_UPDATE_INTERVAL 100000 /* 100ms */ + /* Cache management header --- pointer is NULL until created */ static CatCacheHeader *CacheHdr = NULL; +/* Clock for the last accessed time of a catcache entry. */ +TimestampTz catcacheclock = 0; + static inline HeapTuple SearchCatCacheInternal(CatCache *cache, int nkeys, Datum v1, Datum v2, @@ -473,6 +489,7 @@ CatCacheRemoveCTup(CatCache *cache, CatCTup *ct) /* delink from linked list */ dlist_delete(&ct->cache_elem); + dlist_delete(&ct->lru_node); /* * Free keys when we're dealing with a negative entry, normal entries just @@ -833,6 +850,7 @@ InitCatCache(int id, cp->cc_nkeys = nkeys; for (i = 0; i < nkeys; ++i) cp->cc_keyno[i] = key[i]; + dlist_init(&cp->cc_lru_list); /* * new cache is initialized as far as we can go for now. print some @@ -850,9 +868,83 @@ InitCatCache(int id, */ MemoryContextSwitchTo(oldcxt); + /* initialize catcache reference clock if haven't done yet */ + if (catcacheclock == 0) + catcacheclock = GetCurrentTimestamp(); + return cp; } +/* + * CatCacheCleanupOldEntries - Remove infrequently-used entries + * + * Catcache entries happen to be left unused for a long time for several + * reasons. Remove such entries to prevent catcache from bloating. It is based + * on the similar algorithm with buffer eviction. Entries that are accessed + * several times in a certain period live longer than those that have had less + * access in the same duration. + */ +static bool +CatCacheCleanupOldEntries(CatCache *cp) +{ + int nremoved = 0; + dlist_mutable_iter iter; + + /* Return immediately if disabled */ + if (catalog_cache_prune_min_age == 0) + return false; + + /* Scan over LRU to find entries to remove */ + dlist_foreach_modify(iter, &cp->cc_lru_list) + { + CatCTup *ct = dlist_container(CatCTup, lru_node, iter.cur); + long entry_age; + int us; + + /* Don't remove referenced entries */ + if (ct->refcount != 0 || + (ct->c_list && ct->c_list->refcount != 0)) + continue; + + /* + * Calculate the duration from the time from the last access to + * the "current" time. catcacheclock is updated per-statement + * basis. + */ + TimestampDifference(ct->lastaccess, catcacheclock, &entry_age, &us); + + if (entry_age < catalog_cache_prune_min_age) + { + /* + * We don't have older entries, exit. At least one removal + * prevents rehashing this time. + */ + break; + } + + /* + * Entries that are not accessed after the last pruning are removed in + * that seconds, and their lives are prolonged according to how many + * times they are accessed up to three times of the duration. We don't + * try shrink buckets since pruning effectively caps catcache + * expansion in the long term. + */ + if (ct->naccess > 0) + ct->naccess--; + else + { + CatCacheRemoveCTup(cp, ct); + nremoved++; + } + } + + if (nremoved > 0) + elog(DEBUG1, "pruning catalog cache id=%d for %s: removed %d / %d", + cp->id, cp->cc_relname, nremoved, cp->cc_ntup + nremoved); + + return nremoved > 0; +} + /* * Enlarge a catcache, doubling the number of buckets. */ @@ -1262,7 +1354,21 @@ SearchCatCacheInternal(CatCache *cache, * most frequently accessed elements in any hashbucket will tend to be * near the front of the hashbucket's list.) */ - dlist_move_head(bucket, &ct->cache_elem); + /* dlist_move_head(bucket, &ct->cache_elem);*/ + + /* prolong life of this entry */ + if (ct->naccess < 2) + ct->naccess++; + + /* + * Don't update LRU too frequently. We need to maintain the LRU even + * if pruning is inactive since it can be turned on on-session. + */ + if (catcacheclock - ct->lastaccess > MIN_LRU_UPDATE_INTERVAL) + { + ct->lastaccess = catcacheclock; + dlist_move_tail(&cache->cc_lru_list, &ct->lru_node); + } /* * If it's a positive entry, bump its refcount and return it. If it's @@ -1888,19 +1994,29 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, ct->dead = false; ct->negative = negative; ct->hash_value = hashValue; + ct->naccess = 0; + ct->lastaccess = catcacheclock; + dlist_push_tail(&cache->cc_lru_list, &ct->lru_node); dlist_push_head(&cache->cc_bucket[hashIndex], &ct->cache_elem); cache->cc_ntup++; CacheHdr->ch_ntup++; + /* increase refcount so that the new entry survives pruning */ + ct->refcount++; + /* - * If the hash table has become too full, enlarge the buckets array. Quite - * arbitrarily, we enlarge when fill factor > 2. + * If the hash table has become too full, try removing infrequently used + * entries to make a room for the new entry. If failed, enlarge the bucket + * array instead. Quite arbitrarily, we try this when fill factor > 2. */ - if (cache->cc_ntup > cache->cc_nbuckets * 2) + if (cache->cc_ntup > cache->cc_nbuckets * 2 && + !CatCacheCleanupOldEntries(cache)) RehashCatCache(cache); + ct->refcount--; + return ct; } diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 1766e46037..e671d4428e 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -82,6 +82,7 @@ #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/catcache.h" #include "utils/guc_tables.h" #include "utils/float.h" #include "utils/memutils.h" @@ -2249,6 +2250,17 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"catalog_cache_prune_min_age", PGC_USERSET, RESOURCES_MEM, + gettext_noop("System catalog cache entries that live unused for longer than this seconds are considered for removal."), + gettext_noop("The value of -1 turns off pruning."), + GUC_UNIT_S + }, + &catalog_cache_prune_min_age, + 300, -1, INT_MAX, + NULL, NULL, NULL + }, + /* * We use the hopefully-safely-small value of 100kB as the compiled-in * default for max_stack_depth. InitializeGUCOptions will increase it if diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index bbbeb4bb15..d88ec57382 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -128,6 +128,7 @@ #work_mem = 4MB # min 64kB #maintenance_work_mem = 64MB # min 1MB #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#catalog_cache_prune_min_age = 300s # -1 disables pruning #max_stack_depth = 2MB # min 100kB #shared_memory_type = mmap # the default is the first option # supported by the operating system: diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 65d816a583..a21c53644a 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -22,6 +22,7 @@ #include "access/htup.h" #include "access/skey.h" +#include "datatype/timestamp.h" #include "lib/ilist.h" #include "utils/relcache.h" @@ -61,6 +62,7 @@ typedef struct catcache slist_node cc_next; /* list link */ ScanKeyData cc_skey[CATCACHE_MAXKEYS]; /* precomputed key info for heap * scans */ + dlist_head cc_lru_list; /* * Keep these at the end, so that compiling catcache.c with CATCACHE_STATS @@ -119,6 +121,9 @@ typedef struct catctup bool dead; /* dead but not yet removed? */ bool negative; /* negative cache entry? */ HeapTupleData tuple; /* tuple management header */ + int naccess; /* # of access to this entry, up to 2 */ + TimestampTz lastaccess; /* timestamp of the last usage */ + dlist_node lru_node; /* LRU node */ /* * The tuple may also be a member of at most one CatCList. (If a single @@ -189,6 +194,19 @@ typedef struct catcacheheader /* this extern duplicates utils/memutils.h... */ extern PGDLLIMPORT MemoryContext CacheMemoryContext; +/* for guc.c, not PGDLLPMPORT'ed */ +extern int catalog_cache_prune_min_age; + +/* source clock for access timestamp of catcache entries */ +extern TimestampTz catcacheclock; + +/* SetCatCacheClock - set catcache timestamp source clodk */ +static inline void +SetCatCacheClock(TimestampTz ts) +{ + catcacheclock = ts; +} + extern void CreateCacheMemoryContext(void); extern CatCache *InitCatCache(int id, Oid reloid, Oid indexoid, -- 2.16.3 ----Next_Part(Thu_Apr_04_21_52_55_2019_099)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="0002-Benchmark-extension-for-catcache-pruning-feature.patch" ^ permalink raw reply [nested|flat] 16+ messages in thread
* [PATCH v37 03/11] Allow to prolong life span of transition tables until transaction end @ 2019-12-20 01:09 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Yugo Nagata @ 2019-12-20 01:09 UTC (permalink / raw) Originally, tuplestores of AFTER trigger's transition tables were freed for each query depth. For our IVM implementation, we would like to prolong life of the tuplestores because we have to preserve them for a whole query assuming that some base tables might be changed in some trigger functions. --- src/backend/commands/trigger.c | 80 ++++++++++++++++++++++++++++++++-- src/include/commands/trigger.h | 2 + 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index b87b4b40d07..49fe531198a 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -3819,6 +3819,10 @@ typedef struct AfterTriggerEventList * end of the list, so it is relatively easy to discard them. The event * list chunks themselves are stored in event_cxt. * + * prolonged_tuplestored is a list of transition table tuplestores whose + * life are prolonged to the end of the outmost query instead of each nested + * query. + * * query_depth is the current depth of nested AfterTriggerBeginQuery calls * (-1 when the stack is empty). * @@ -3884,6 +3888,7 @@ typedef struct AfterTriggersData SetConstraintState state; /* the active S C state */ AfterTriggerEventList events; /* deferred-event list */ MemoryContext event_cxt; /* memory context for events, if any */ + List *prolonged_tuplestores; /* list of prolonged tuplestores */ /* per-query-level data: */ AfterTriggersQueryData *query_stack; /* array of structs shown below */ @@ -3932,6 +3937,7 @@ struct AfterTriggersTableData bool closed; /* true when no longer OK to add tuples */ bool before_trig_done; /* did we already queue BS triggers? */ bool after_trig_done; /* did we already queue AS triggers? */ + bool prolonged; /* are transition tables prolonged? */ AfterTriggerEventList after_trig_events; /* if so, saved list pointer */ /* "old" transition table for UPDATE/DELETE, if any */ @@ -3978,6 +3984,7 @@ static void TransitionTableAddTuple(EState *estate, TupleTableSlot *original_insert_tuple, Tuplestorestate *tuplestore); static void AfterTriggerFreeQuery(AfterTriggersQueryData *qs); +static void release_or_prolong_tuplestore(Tuplestorestate *ts, bool prolonged); static SetConstraintState SetConstraintStateCreate(int numalloc); static SetConstraintState SetConstraintStateCopy(SetConstraintState origstate); static SetConstraintState SetConstraintStateAddItem(SetConstraintState state, @@ -4873,6 +4880,45 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events, } +/* + * SetTransitionTablePreserved + * + * Prolong lifespan of transition tables corresponding specified relid and + * command type to the end of the outmost query instead of each nested query. + * This enables to use nested AFTER trigger's transition tables from outer + * query's triggers. Currently, only immediate incremental view maintenance + * uses this. + */ +void +SetTransitionTablePreserved(Oid relid, CmdType cmdType) +{ + AfterTriggersTableData *table; + AfterTriggersQueryData *qs; + bool found = false; + ListCell *lc; + + /* Check state, like AfterTriggerSaveEvent. */ + if (afterTriggers.query_depth < 0) + elog(ERROR, "SetTransitionTablePreserved() called outside of query"); + + qs = &afterTriggers.query_stack[afterTriggers.query_depth]; + + foreach(lc, qs->tables) + { + table = (AfterTriggersTableData *) lfirst(lc); + if (table->relid == relid && table->cmdType == cmdType && + table->closed) + { + table->prolonged = true; + found = true; + } + } + + if (!found) + elog(ERROR,"could not find table with OID %d and command type %d", relid, cmdType); +} + + /* * GetAfterTriggersTableData * @@ -5113,6 +5159,7 @@ AfterTriggerBeginXact(void) afterTriggers.firing_depth = 0; afterTriggers.batch_callbacks = NIL; afterTriggers.firing_batch_callbacks = false; + afterTriggers.prolonged_tuplestores = NIL; /* * Verify that there is no leftover state remaining. If these assertions @@ -5287,11 +5334,11 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs) ts = table->old_tuplestore; table->old_tuplestore = NULL; if (ts) - tuplestore_end(ts); + release_or_prolong_tuplestore(ts, table->prolonged); ts = table->new_tuplestore; table->new_tuplestore = NULL; if (ts) - tuplestore_end(ts); + release_or_prolong_tuplestore(ts, table->prolonged); if (table->storeslot) { TupleTableSlot *slot = table->storeslot; @@ -5309,8 +5356,33 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs) qs->tables = NIL; list_free_deep(tables); - list_free_deep(qs->batch_callbacks); - qs->batch_callbacks = NIL; + /* Release prolonged tuplestores at the end of the outmost query */ + if (afterTriggers.query_depth == 0) + { + foreach(lc, afterTriggers.prolonged_tuplestores) + { + ts = (Tuplestorestate *) lfirst(lc); + if (ts) + tuplestore_end(ts); + } + afterTriggers.prolonged_tuplestores = NIL; + } +} + +/* + * Release the tuplestore, or append it to the prolonged tuplestores list. + */ +static void +release_or_prolong_tuplestore(Tuplestorestate *ts, bool prolonged) +{ + if (prolonged && afterTriggers.query_depth > 0) + { + MemoryContext oldcxt = MemoryContextSwitchTo(CurTransactionContext); + afterTriggers.prolonged_tuplestores = lappend(afterTriggers.prolonged_tuplestores, ts); + MemoryContextSwitchTo(oldcxt); + } + else + tuplestore_end(ts); } diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index 1d9869973c0..e5dd915096d 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -269,6 +269,8 @@ extern void AfterTriggerEndSubXact(bool isCommit); extern void AfterTriggerSetState(ConstraintsSetStmt *stmt); extern bool AfterTriggerPendingOnRel(Oid relid); +extern void SetTransitionTablePreserved(Oid relid, CmdType cmdType); + /* * in utils/adt/ri_triggers.c -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0002-Add-relisivm-column-to-pg_class-system-catalog.patch" Content-Disposition: attachment; filename="v37-0002-Add-relisivm-column-to-pg_class-system-catalog.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 16+ messages in thread
* Re: [(known) BUG] DELETE/UPDATE more than one row in partitioned foreign table @ 2026-04-21 11:34 Nikita Malakhov <[email protected]> 0 siblings, 0 replies; 16+ messages in thread From: Nikita Malakhov @ 2026-04-21 11:34 UTC (permalink / raw) To: Etsuro Fujita <[email protected]>; +Cc: Jehan-Guillaume de Rorthais <[email protected]>; [email protected] Hi hackers! We've been bitten by this bug recently too. So I've taken off from the previous approach, have fixed some issues and developed it a but further, please check this POC patch. While working on it I've stumbled upon a bunch of checks inside copy slot/tuple machinery which considered only case when source and destination match, with an old commentary that this is not very good and has to be improved, so I've slightly modified this functionality for my purposes. On Wed, Aug 6, 2025 at 2:25 PM Etsuro Fujita <[email protected]> wrote: > On Wed, Jul 30, 2025 at 12:48 AM Jehan-Guillaume de Rorthais > <[email protected]> wrote: > > On Wed, 23 Jul 2025 19:38:19 +0900 > > Etsuro Fujita <[email protected]> wrote: > > > On Sat, Jul 19, 2025 at 12:53 AM Jehan-Guillaume de Rorthais > > > <[email protected]> wrote: > > > > Or maybe we should just not support foreign table to reference a > > > > remote partitioned table? > > > > > > I don't think so because we can execute SELECT, INSERT, and direct > > > UPDATE/DELETE safely on such a foreign table. > > > > Sure, but it's still possible to create one local foreign partition > pointing to > > remote foreign equivalent. And it seems safer considering how hard it > seems to > > keep corruptions away from the current situation. > > Yeah, that would be a simple workaround for this issue. > > > > I think a simple fix for this is to teach the system that the foreign > > > table is a partitioned table; in more detail, I would like to propose > > > to 1) add to postgres_fdw a table option, inherited, to indicate > > > whether the foreign table is a partitioned/inherited table or not, and > > > 2) modify postgresPlanForeignModify() to throw an error if the given > > > operation is an update/delete on such a foreign table. Attached is a > > > WIP patch for that. I think it is the user's responsibility to set > > > the option properly, but we could modify postgresImportForeignSchema() > > > to support that. Also, I think this would be back-patchable. > > > > So it's just a flag the user must set to allow/disallow UPDATE/DELETE on > a > > foreign table. I'm not convinced by this solution as users can still > > easily corrupt their data just because they overlooked the documentation. > > > > What about the first solution Ashutosh Bapat was suggesting: «Use WHERE > CURRENT > > OF with cursors to update rows.» ? > > > https://www.postgresql.org/message-id/CAFjFpRfcgwsHRmpvoOK-GUQi-n8MgAS%2BOxcQo%3DaBDn1COywmcg%40mail... > > > > It seems to me it never has been explored, is it? > > My concern about that solution is: as mentioned by him, it requires > fetching only one row from the remote at a time, which would lead to > large performance degradation when updating many rows. > > Best regards, > Etsuro Fujita > > > -- Regards, Nikita Malakhov Postgres Professional The Russian Postgres Company https://postgrespro.ru/ Attachments: [application/octet-stream] v1-0003-fdw-del-upd-using-tableoid.patch (92.3K, ../../CAN-LCVMz58ukZ7ubGXiLuTeFE7wWmSwDw4URpF0q1ejzRvqbzg@mail.gmail.com/3-v1-0003-fdw-del-upd-using-tableoid.patch) download | inline diff: From 8e8d186585c1716f6573e9e0593bdec6badbc7e4 Mon Sep 17 00:00:00 2001 From: Nikita Malakhov <[email protected]> Date: Tue, 21 Apr 2026 14:21:13 +0300 Subject: [PATCH] [POC] This patch modifies FDW engine to use remote table OID for DELETE and UPDATE of partitioned tables. [1] https://www.postgresql.org/message-id/flat/CAPmGK15CQK-oYFMAyq%2BrR0rQapUHtvAGuGgY5ahERHzZ4tmC8g%40mail.gmail.com#4321975bbd1af71c78d45d9a441e8458 --- contrib/postgres_fdw/deparse.c | 85 ++- .../postgres_fdw/expected/postgres_fdw.out | 508 ++++++++++-------- contrib/postgres_fdw/postgres_fdw.c | 308 ++++++++++- contrib/postgres_fdw/postgres_fdw.h | 3 + contrib/postgres_fdw/sql/postgres_fdw.sql | 44 ++ 5 files changed, 700 insertions(+), 248 deletions(-) diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index 2dcc6c8af1b..8d566bce4bf 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -48,6 +48,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "commands/defrem.h" +#include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "nodes/plannodes.h" #include "optimizer/optimizer.h" @@ -132,6 +133,7 @@ static void deparseTargetList(StringInfo buf, Relation rel, bool is_returning, Bitmapset *attrs_used, + bool tableoid_needed, bool qualify_col, List **retrieved_attrs); static void deparseExplicitTargetList(List *tlist, @@ -350,10 +352,11 @@ foreign_expr_walker(Node *node, /* Var belongs to foreign table */ /* - * System columns other than ctid should not be sent to - * the remote, since we don't make any effort to ensure - * that local and remote values match (tableoid, in - * particular, almost certainly doesn't match). + * System columns other than ctid and remote table oid + * should not be sent to the remote, since we don't make + * any effort to ensure that local and remote values + * match (tableoid, in particular, almost certainly + * doesn't match). */ if (var->varattno < 0 && var->varattno != SelfItemPointerAttributeNumber) @@ -1235,6 +1238,23 @@ build_tlist_to_deparse(RelOptInfo *foreignrel) PVC_RECURSE_PLACEHOLDERS)); } + /* Also, add the Param representing the remote table OID, if it exists. */ + if (fpinfo->tableoid_param) + { + TargetEntry *tle; + /* + * Core code should have contained the Param in the given relation's + * reltarget. + */ + Assert(list_member(foreignrel->reltarget->exprs, + fpinfo->tableoid_param)); + tle = makeTargetEntry((Expr *) copyObject(fpinfo->tableoid_param), + list_length(tlist) + 1, + NULL, + false); + tlist = lappend(tlist, tle); + } + return tlist; } @@ -1390,7 +1410,9 @@ deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs, Relation rel = table_open(rte->relid, NoLock); deparseTargetList(buf, rte, foreignrel->relid, rel, false, - fpinfo->attrs_used, false, retrieved_attrs); + fpinfo->attrs_used, + fpinfo->tableoid_param != NULL, + false, retrieved_attrs); table_close(rel, NoLock); } } @@ -1441,6 +1463,7 @@ deparseTargetList(StringInfo buf, Relation rel, bool is_returning, Bitmapset *attrs_used, + bool tableoid_needed, bool qualify_col, List **retrieved_attrs) { @@ -1499,6 +1522,22 @@ deparseTargetList(StringInfo buf, SelfItemPointerAttributeNumber); } + if (tableoid_needed && + (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber, + attrs_used))) + { + Assert(!first); + Assert(!is_returning); + + appendStringInfoString(buf, ", "); + if (qualify_col) + ADD_REL_QUALIFIER(buf, rtindex); + appendStringInfoString(buf, "tableoid"); + + *retrieved_attrs = lappend_int(*retrieved_attrs, + TableOidAttributeNumber); + } + /* Don't generate bad syntax if no undropped columns */ if (first && !is_returning) appendStringInfoString(buf, "NULL"); @@ -2259,7 +2298,7 @@ deparseUpdateSql(StringInfo buf, RangeTblEntry *rte, deparseRelation(buf, rel); appendStringInfoString(buf, " SET "); - pindex = 2; /* ctid is always the first param */ + pindex = 3; /* ctid is always the first param */ first = true; foreach(lc, targetAttrs) { @@ -2279,7 +2318,7 @@ deparseUpdateSql(StringInfo buf, RangeTblEntry *rte, pindex++; } } - appendStringInfoString(buf, " WHERE ctid = $1"); + appendStringInfoString(buf, " WHERE ctid = $1 AND tableoid = $2"); deparseReturningList(buf, rte, rtindex, rel, rel->trigdesc && rel->trigdesc->trig_update_after_row, @@ -2397,7 +2436,7 @@ deparseDeleteSql(StringInfo buf, RangeTblEntry *rte, { appendStringInfoString(buf, "DELETE FROM "); deparseRelation(buf, rel); - appendStringInfoString(buf, " WHERE ctid = $1"); + appendStringInfoString(buf, " WHERE ctid = $1 AND tableoid = $2"); deparseReturningList(buf, rte, rtindex, rel, rel->trigdesc && rel->trigdesc->trig_delete_after_row, @@ -2512,7 +2551,7 @@ deparseReturningList(StringInfo buf, RangeTblEntry *rte, } if (attrs_used != NULL) - deparseTargetList(buf, rte, rtindex, rel, true, attrs_used, false, + deparseTargetList(buf, rte, rtindex, rel, true, attrs_used, false, false, retrieved_attrs); else *retrieved_attrs = NIL; @@ -2719,6 +2758,12 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte, ADD_REL_QUALIFIER(buf, varno); appendStringInfoString(buf, "ctid"); } + else if (varattno == TableOidAttributeNumber) + { + if (qualify_col) + ADD_REL_QUALIFIER(buf, varno); + appendStringInfoString(buf, "tableoid"); + } else if (varattno < 0) { /* @@ -2730,8 +2775,9 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte, Oid fetchval = 0; if (varattno == TableOidAttributeNumber) + { fetchval = rte->relid; - + } if (qualify_col) { appendStringInfoString(buf, "CASE WHEN ("); @@ -2782,7 +2828,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte, appendStringInfoString(buf, "ROW("); deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col, - &retrieved_attrs); + qualify_col, &retrieved_attrs); appendStringInfoChar(buf, ')'); /* Complete the CASE WHEN statement started above. */ @@ -3167,6 +3213,23 @@ deparseConst(Const *node, deparse_expr_cxt *context, int showtype) static void deparseParam(Param *node, deparse_expr_cxt *context) { + PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) context->foreignrel->fdw_private; + + /* + * If the Param is the one representing the remote table OID, the value + * needs to be produced; fetch the remote table OID, instead. + */ + if (equal(node, (Node *) fpinfo->tableoid_param)) + { + Assert(bms_is_member(context->root->parse->resultRelation, + context->foreignrel->relids)); + Assert(bms_membership(context->foreignrel->relids) == BMS_MULTIPLE); + ADD_REL_QUALIFIER(context->buf, context->root->parse->resultRelation); + + appendStringInfoString(context->buf, "tableoid"); + return; + } + if (context->params_list) { int pindex = 0; diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 10e87acabef..8b93f6785fc 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -5275,14 +5275,14 @@ BEGIN; EXPLAIN (verbose, costs off) UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7b' WHERE c1 % 10 = 7 AND c1 < 40 RETURNING old.*, new.*; -- can't be pushed down - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 Output: old.c1, old.c2, old.c3, old.c4, old.c5, old.c6, old.c7, old.c8, new.c1, new.c2, new.c3, new.c4, new.c5, new.c6, new.c7, new.c8 - Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + Remote SQL: UPDATE "S 1"."T 1" SET c2 = $3, c3 = $4 WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 -> Foreign Scan on public.ft2 - Output: (c2 + 400), (c3 || '_update7b'::text), ctid, ft2.* - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" < 40)) AND ((("C 1" % 10) = 7)) FOR UPDATE + Output: (c2 + 400), (c3 || '_update7b'::text), ctid, tableoid, $0, ft2.* + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid, tableoid FROM "S 1"."T 1" WHERE (("C 1" < 40)) AND ((("C 1" % 10) = 7)) FOR UPDATE (6 rows) UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7b' WHERE c1 % 10 = 7 AND c1 < 40 @@ -5429,14 +5429,14 @@ DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4; BEGIN; EXPLAIN (verbose, costs off) DELETE FROM ft2 WHERE c1 % 10 = 6 AND c1 < 40 RETURNING old.c1, c4; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------------------------------------------ + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------- Delete on public.ft2 Output: old.c1, c4 - Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 RETURNING "C 1", c4 + Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c4 -> Foreign Scan on public.ft2 - Output: ctid - Remote SQL: SELECT ctid FROM "S 1"."T 1" WHERE (("C 1" < 40)) AND ((("C 1" % 10) = 6)) FOR UPDATE + Output: ctid, tableoid, $0 + Remote SQL: SELECT ctid, tableoid FROM "S 1"."T 1" WHERE (("C 1" < 40)) AND ((("C 1" % 10) = 6)) FOR UPDATE (6 rows) DELETE FROM ft2 WHERE c1 % 10 = 6 AND c1 < 40 RETURNING old.c1, c4; @@ -6400,27 +6400,27 @@ BEGIN; FROM ft4 INNER JOIN ft5 ON (ft4.c1 = ft5.c1) WHERE ft2.c1 > 1200 AND ft2.c2 = ft4.c1 RETURNING old, new, ft2, ft2.*, ft4, ft4.*; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 Output: old.*, new.*, ft2.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.*, ft4.c1, ft4.c2, ft4.c3 - Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + Remote SQL: UPDATE "S 1"."T 1" SET c3 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 -> Foreign Scan - Output: 'bar'::text, ft2.ctid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3 + Output: 'bar'::text, ft2.ctid, ft2.tableoid, ($0), ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3 Relations: ((public.ft2) INNER JOIN (public.ft4)) INNER JOIN (public.ft5) - Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.c1, r2.c2, r2.c3) END, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r2.c1, r2.c2, r2.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 1200)))) INNER JOIN "S 1"."T 4" r3 ON (((r2.c1 = r3.c1)))) FOR UPDATE OF r1 + Remote SQL: SELECT r1.ctid, r1.tableoid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.c1, r2.c2, r2.c3) END, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r2.c1, r2.c2, r2.c3, r1.tableoid FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 1200)))) INNER JOIN "S 1"."T 4" r3 ON (((r2.c1 = r3.c1)))) FOR UPDATE OF r1 -> Nested Loop - Output: ft2.ctid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3 + Output: ft2.ctid, ft2.tableoid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3, ($0) Join Filter: (ft4.c1 = ft5.c1) -> Sort - Output: ft2.ctid, ft2.*, ft2.c2, ft4.*, ft4.c1, ft4.c2, ft4.c3 + Output: ft2.ctid, ft2.tableoid, ft2.*, ($0), ft2.c2, ft4.*, ft4.c1, ft4.c2, ft4.c3 Sort Key: ft2.c2 -> Hash Join - Output: ft2.ctid, ft2.*, ft2.c2, ft4.*, ft4.c1, ft4.c2, ft4.c3 + Output: ft2.ctid, ft2.tableoid, ft2.*, ($0), ft2.c2, ft4.*, ft4.c1, ft4.c2, ft4.c3 Hash Cond: (ft2.c2 = ft4.c1) -> Foreign Scan on public.ft2 - Output: ft2.ctid, ft2.*, ft2.c2 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1200)) FOR UPDATE + Output: ft2.ctid, ft2.tableoid, ft2.*, $0, ft2.c2 + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid, tableoid FROM "S 1"."T 1" WHERE (("C 1" > 1200)) FOR UPDATE -> Hash Output: ft4.*, ft4.c1, ft4.c2, ft4.c3 -> Foreign Scan on public.ft4 @@ -6498,13 +6498,13 @@ UPDATE ft2 AS target SET (c2, c7) = ( FROM ft2 AS src WHERE target.c1 = src.c1 ) WHERE c1 > 1100; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------ + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 target - Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c7 = $3 WHERE ctid = $1 + Remote SQL: UPDATE "S 1"."T 1" SET c2 = $3, c7 = $4 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.ft2 target - Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), target.ctid, target.* - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1100)) FOR UPDATE + Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), target.ctid, target.tableoid, $3, target.* + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid, tableoid FROM "S 1"."T 1" WHERE (("C 1" > 1100)) FOR UPDATE SubPlan multiexpr_1 -> Foreign Scan on public.ft2 src Output: (src.c2 * 10), src.c7 @@ -6526,20 +6526,20 @@ UPDATE ft2 AS target SET (c2) = ( EXPLAIN (VERBOSE, COSTS OFF) UPDATE ft2 d SET c2 = CASE WHEN random() >= 0 THEN d.c2 ELSE 0 END FROM ft2 AS t WHERE d.c1 = t.c1 AND d.c1 > 1000; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 d - Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2 WHERE ctid = $1 + Remote SQL: UPDATE "S 1"."T 1" SET c2 = $3 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan - Output: CASE WHEN (random() >= '0'::double precision) THEN d.c2 ELSE 0 END, d.ctid, d.*, t.* + Output: CASE WHEN (random() >= '0'::double precision) THEN d.c2 ELSE 0 END, d.ctid, d.tableoid, ($0), d.*, t.* Relations: (public.ft2 d) INNER JOIN (public.ft2 t) - Remote SQL: SELECT r1.c2, r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1."C 1" > 1000)))) FOR UPDATE OF r1 + Remote SQL: SELECT r1.c2, r1.ctid, r1.tableoid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END, r1.tableoid FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1."C 1" > 1000)))) FOR UPDATE OF r1 -> Hash Join - Output: d.c2, d.ctid, d.*, t.* + Output: d.c2, d.ctid, d.tableoid, d.*, t.*, ($0) Hash Cond: (d.c1 = t.c1) -> Foreign Scan on public.ft2 d - Output: d.c2, d.ctid, d.*, d.c1 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1000)) ORDER BY "C 1" ASC NULLS LAST FOR UPDATE + Output: d.c2, d.ctid, d.tableoid, d.*, $0, d.c1 + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid, tableoid FROM "S 1"."T 1" WHERE (("C 1" > 1000)) ORDER BY "C 1" ASC NULLS LAST FOR UPDATE -> Hash Output: t.*, t.c1 -> Foreign Scan on public.ft2 t @@ -6559,19 +6559,19 @@ EXPLAIN (verbose, costs off) WITH cte AS ( UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING * ) SELECT * FROM cte ORDER BY c1; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------ Sort Output: cte.c1, cte.c2, cte.c3, cte.c4, cte.c5, cte.c6, cte.c7, cte.c8 Sort Key: cte.c1 CTE cte -> Update on public.ft2 Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8 - Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + Remote SQL: UPDATE "S 1"."T 1" SET c3 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 -> Foreign Scan on public.ft2 - Output: 'bar'::text, ft2.ctid, ft2.* + Output: 'bar'::text, ft2.ctid, ft2.tableoid, $0, ft2.* Filter: (postgres_fdw_abs(ft2.c1) > 2000) - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" FOR UPDATE + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid, tableoid FROM "S 1"."T 1" FOR UPDATE -> CTE Scan on cte Output: cte.c1, cte.c2, cte.c3, cte.c4, cte.c5, cte.c6, cte.c7, cte.c8 (13 rows) @@ -6602,13 +6602,13 @@ UPDATE ft2 SET c3 = 'baz' ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3 - Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + Remote SQL: UPDATE "S 1"."T 1" SET c3 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 -> Nested Loop - Output: 'baz'::text, ft2.ctid, ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3 + Output: 'baz'::text, ft2.ctid, ft2.tableoid, ($0), ft2.*, ft4.*, ft5.*, ft4.c1, ft4.c2, ft4.c3, ft5.c1, ft5.c2, ft5.c3 Join Filter: (ft2.c2 === ft4.c1) -> Foreign Scan on public.ft2 - Output: ft2.ctid, ft2.*, ft2.c2 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE + Output: ft2.ctid, ft2.tableoid, ft2.*, $0, ft2.c2 + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid, tableoid FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE -> Foreign Scan Output: ft4.*, ft4.c1, ft4.c2, ft4.c3, ft5.*, ft5.c1, ft5.c2, ft5.c3 Relations: (public.ft4) INNER JOIN (public.ft5) @@ -6640,24 +6640,24 @@ DELETE FROM ft2 USING ft4 INNER JOIN ft5 ON (ft4.c1 === ft5.c1) WHERE ft2.c1 > 2000 AND ft2.c2 = ft4.c1 RETURNING ft2.c1, ft2.c2, ft2.c3; -- can't be pushed down - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Delete on public.ft2 Output: ft2.c1, ft2.c2, ft2.c3 - Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 RETURNING "C 1", c2, c3 + Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 AND tableoid = $2 RETURNING "C 1", c2, c3 -> Foreign Scan - Output: ft2.ctid, ft4.*, ft5.* + Output: ft2.ctid, ft2.tableoid, ($0), ft4.*, ft5.* Filter: (ft4.c1 === ft5.c1) Relations: ((public.ft2) INNER JOIN (public.ft4)) INNER JOIN (public.ft5) - Remote SQL: SELECT r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.c1, r2.c2, r2.c3) END, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r2.c1, r3.c1 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 2000)))) INNER JOIN "S 1"."T 4" r3 ON (TRUE)) FOR UPDATE OF r1 + Remote SQL: SELECT r1.ctid, r1.tableoid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.c1, r2.c2, r2.c3) END, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2, r3.c3) END, r2.c1, r3.c1, r1.tableoid FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c2 = r2.c1)) AND ((r1."C 1" > 2000)))) INNER JOIN "S 1"."T 4" r3 ON (TRUE)) FOR UPDATE OF r1 -> Nested Loop - Output: ft2.ctid, ft4.*, ft5.*, ft4.c1, ft5.c1 + Output: ft2.ctid, ft2.tableoid, ft4.*, ft5.*, ft4.c1, ft5.c1, ($0) -> Nested Loop - Output: ft2.ctid, ft4.*, ft4.c1 + Output: ft2.ctid, ft2.tableoid, ($0), ft4.*, ft4.c1 Join Filter: (ft2.c2 = ft4.c1) -> Foreign Scan on public.ft2 - Output: ft2.ctid, ft2.c2 - Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE + Output: ft2.ctid, ft2.tableoid, $0, ft2.c2 + Remote SQL: SELECT c2, ctid, tableoid FROM "S 1"."T 1" WHERE (("C 1" > 2000)) FOR UPDATE -> Foreign Scan on public.ft4 Output: ft4.*, ft4.c1 Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 3" @@ -7169,19 +7169,19 @@ SET enable_hashjoin TO false; SET enable_material TO false; EXPLAIN (VERBOSE, COSTS OFF) UPDATE remt2 SET c2 = remt2.c2 || remt2.c2 FROM loct1 WHERE loct1.c1 = remt2.c1 RETURNING remt2.*; - QUERY PLAN --------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------- Update on public.remt2 Output: remt2.c1, remt2.c2 - Remote SQL: UPDATE public.loct2 SET c2 = $2 WHERE ctid = $1 RETURNING c1, c2 + Remote SQL: UPDATE public.loct2 SET c2 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING c1, c2 -> Nested Loop - Output: (remt2.c2 || remt2.c2), remt2.ctid, remt2.*, loct1.ctid + Output: (remt2.c2 || remt2.c2), remt2.ctid, remt2.tableoid, ($0), remt2.*, loct1.ctid Join Filter: (remt2.c1 = loct1.c1) -> Seq Scan on public.loct1 Output: loct1.ctid, loct1.c1 -> Foreign Scan on public.remt2 - Output: remt2.c2, remt2.ctid, remt2.*, remt2.c1 - Remote SQL: SELECT c1, c2, ctid FROM public.loct2 FOR UPDATE + Output: remt2.c2, remt2.ctid, remt2.tableoid, remt2.*, $0, remt2.c1 + Remote SQL: SELECT c1, c2, ctid, tableoid FROM public.loct2 FOR UPDATE (11 rows) UPDATE remt2 SET c2 = remt2.c2 || remt2.c2 FROM loct1 WHERE loct1.c1 = remt2.c1 RETURNING remt2.*; @@ -7342,13 +7342,13 @@ SELECT * FROM foreign_tbl; EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = b + 5; - QUERY PLAN ---------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------- Update on public.foreign_tbl - Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.base_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Foreign Scan on public.foreign_tbl - Output: (foreign_tbl.b + 5), foreign_tbl.ctid, foreign_tbl.* - Remote SQL: SELECT a, b, ctid FROM public.base_tbl WHERE ((a < b)) FOR UPDATE + Output: (foreign_tbl.b + 5), foreign_tbl.ctid, foreign_tbl.tableoid, $0, foreign_tbl.* + Remote SQL: SELECT a, b, ctid, tableoid FROM public.base_tbl WHERE ((a < b)) FOR UPDATE (5 rows) UPDATE rw_view SET b = b + 5; -- should fail @@ -7356,13 +7356,13 @@ ERROR: new row violates check option for view "rw_view" DETAIL: Failing row contains (20, 20). EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = b + 15; - QUERY PLAN ---------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------- Update on public.foreign_tbl - Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.base_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Foreign Scan on public.foreign_tbl - Output: (foreign_tbl.b + 15), foreign_tbl.ctid, foreign_tbl.* - Remote SQL: SELECT a, b, ctid FROM public.base_tbl WHERE ((a < b)) FOR UPDATE + Output: (foreign_tbl.b + 15), foreign_tbl.ctid, foreign_tbl.tableoid, $0, foreign_tbl.* + Remote SQL: SELECT a, b, ctid, tableoid FROM public.base_tbl WHERE ((a < b)) FOR UPDATE (5 rows) UPDATE rw_view SET b = b + 15; -- ok @@ -7455,14 +7455,14 @@ SELECT * FROM foreign_tbl; EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = b + 5; - QUERY PLAN ------------------------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Foreign Update on public.foreign_tbl parent_tbl_1 - Remote SQL: UPDATE public.child_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.child_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Foreign Scan on public.foreign_tbl parent_tbl_1 - Output: (parent_tbl_1.b + 5), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* - Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE + Output: (parent_tbl_1.b + 5), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.tableoid, $0, parent_tbl_1.* + Remote SQL: SELECT a, b, ctid, tableoid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE (6 rows) UPDATE rw_view SET b = b + 5; -- should fail @@ -7470,14 +7470,14 @@ ERROR: new row violates check option for view "rw_view" DETAIL: Failing row contains (20, 20). EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = b + 15; - QUERY PLAN -------------------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Foreign Update on public.foreign_tbl parent_tbl_1 - Remote SQL: UPDATE public.child_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.child_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Foreign Scan on public.foreign_tbl parent_tbl_1 - Output: (parent_tbl_1.b + 15), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* - Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE + Output: (parent_tbl_1.b + 15), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.tableoid, $0, parent_tbl_1.* + Remote SQL: SELECT a, b, ctid, tableoid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE (6 rows) UPDATE rw_view SET b = b + 15; -- ok @@ -7526,14 +7526,14 @@ CREATE VIEW rw_view AS SELECT * FROM parent_tbl WHERE a < 5 WITH CHECK OPTION; INSERT INTO parent_tbl (a) VALUES(1),(5); EXPLAIN (VERBOSE, COSTS OFF) UPDATE rw_view SET b = 'text', c = 123.456; - QUERY PLAN -------------------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Foreign Update on public.child_foreign parent_tbl_1 - Remote SQL: UPDATE public.child_local SET b = $2, c = $3 WHERE ctid = $1 RETURNING a + Remote SQL: UPDATE public.child_local SET b = $3, c = $4 WHERE ctid = $1 AND tableoid = $2 RETURNING a -> Foreign Scan on public.child_foreign parent_tbl_1 - Output: 'text'::text, 123.456, parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* - Remote SQL: SELECT b, c, a, ctid FROM public.child_local WHERE ((a < 5)) FOR UPDATE + Output: 'text'::text, 123.456, parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.tableoid, $0, parent_tbl_1.* + Remote SQL: SELECT b, c, a, ctid, tableoid FROM public.child_local WHERE ((a < 5)) FOR UPDATE (6 rows) UPDATE rw_view SET b = 'text', c = 123.456; @@ -7612,13 +7612,13 @@ insert into grem1 (a) values (1), (2); insert into grem1 (a) values (1), (2); explain (verbose, costs off) update grem1 set a = 22 where a = 2; - QUERY PLAN ----------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------- Update on public.grem1 - Remote SQL: UPDATE public.gloc1 SET a = $2, b = DEFAULT, c = DEFAULT WHERE ctid = $1 + Remote SQL: UPDATE public.gloc1 SET a = $3, b = DEFAULT, c = DEFAULT WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.grem1 - Output: 22, ctid, grem1.* - Remote SQL: SELECT a, b, c, ctid FROM public.gloc1 WHERE ((a = 2)) FOR UPDATE + Output: 22, ctid, tableoid, $0, grem1.* + Remote SQL: SELECT a, b, c, ctid, tableoid FROM public.gloc1 WHERE ((a = 2)) FOR UPDATE (5 rows) update grem1 set a = 22 where a = 2; @@ -7945,13 +7945,13 @@ SELECT * from loc1; EXPLAIN (verbose, costs off) UPDATE rem1 set f1 = 10; -- all columns should be transmitted - QUERY PLAN ------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------------------------- Update on public.rem1 - Remote SQL: UPDATE public.loc1 SET f1 = $2, f2 = $3 WHERE ctid = $1 + Remote SQL: UPDATE public.loc1 SET f1 = $3, f2 = $4 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.rem1 - Output: 10, ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: 10, ctid, tableoid, $0, rem1.* + Remote SQL: SELECT f1, f2, ctid, tableoid FROM public.loc1 FOR UPDATE (5 rows) UPDATE rem1 set f1 = 10; @@ -8093,12 +8093,12 @@ DELETE FROM rem1; -- can be pushed down EXPLAIN (verbose, costs off) DELETE FROM rem1 WHERE false; -- currently can't be pushed down - QUERY PLAN -------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------- Delete on public.rem1 - Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 + Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 AND tableoid = $2 -> Result - Output: ctid + Output: ctid, tableoid, $0 Replaces: Scan on rem1 One-Time Filter: false (6 rows) @@ -8199,13 +8199,13 @@ BEFORE UPDATE ON rem1 FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); EXPLAIN (verbose, costs off) UPDATE rem1 set f2 = ''; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------------------------- Update on public.rem1 - Remote SQL: UPDATE public.loc1 SET f1 = $2, f2 = $3 WHERE ctid = $1 + Remote SQL: UPDATE public.loc1 SET f1 = $3, f2 = $4 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.rem1 - Output: ''::text, ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: ''::text, ctid, tableoid, $0, rem1.* + Remote SQL: SELECT f1, f2, ctid, tableoid FROM public.loc1 FOR UPDATE (5 rows) EXPLAIN (verbose, costs off) @@ -8223,13 +8223,13 @@ AFTER UPDATE ON rem1 FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); EXPLAIN (verbose, costs off) UPDATE rem1 set f2 = ''; -- can't be pushed down - QUERY PLAN -------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------- Update on public.rem1 - Remote SQL: UPDATE public.loc1 SET f2 = $2 WHERE ctid = $1 RETURNING f1, f2 + Remote SQL: UPDATE public.loc1 SET f2 = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING f1, f2 -> Foreign Scan on public.rem1 - Output: ''::text, ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: ''::text, ctid, tableoid, $0, rem1.* + Remote SQL: SELECT f1, f2, ctid, tableoid FROM public.loc1 FOR UPDATE (5 rows) EXPLAIN (verbose, costs off) @@ -8257,13 +8257,13 @@ UPDATE rem1 set f2 = ''; -- can be pushed down EXPLAIN (verbose, costs off) DELETE FROM rem1; -- can't be pushed down - QUERY PLAN ---------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------- Delete on public.rem1 - Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 + Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.rem1 - Output: ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: ctid, tableoid, $0, rem1.* + Remote SQL: SELECT f1, f2, ctid, tableoid FROM public.loc1 FOR UPDATE (5 rows) DROP TRIGGER trig_row_before_delete ON rem1; @@ -8281,13 +8281,13 @@ UPDATE rem1 set f2 = ''; -- can be pushed down EXPLAIN (verbose, costs off) DELETE FROM rem1; -- can't be pushed down - QUERY PLAN ------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------ Delete on public.rem1 - Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 RETURNING f1, f2 + Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 AND tableoid = $2 RETURNING f1, f2 -> Foreign Scan on public.rem1 - Output: ctid, rem1.* - Remote SQL: SELECT f1, f2, ctid FROM public.loc1 FOR UPDATE + Output: ctid, tableoid, $0, rem1.* + Remote SQL: SELECT f1, f2, ctid, tableoid FROM public.loc1 FOR UPDATE (5 rows) DROP TRIGGER trig_row_after_delete ON rem1; @@ -8324,28 +8324,28 @@ CONTEXT: COPY parent_tbl, line 1: "AAA 42" ALTER SERVER loopback OPTIONS (DROP batch_size); EXPLAIN (VERBOSE, COSTS OFF) UPDATE parent_tbl SET b = b + 1; - QUERY PLAN ------------------------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Foreign Update on public.foreign_tbl parent_tbl_1 - Remote SQL: UPDATE public.local_tbl SET b = $2 WHERE ctid = $1 + Remote SQL: UPDATE public.local_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.foreign_tbl parent_tbl_1 - Output: (parent_tbl_1.b + 1), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* - Remote SQL: SELECT a, b, ctid FROM public.local_tbl FOR UPDATE + Output: (parent_tbl_1.b + 1), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.tableoid, $0, parent_tbl_1.* + Remote SQL: SELECT a, b, ctid, tableoid FROM public.local_tbl FOR UPDATE (6 rows) UPDATE parent_tbl SET b = b + 1; ERROR: cannot collect transition tuples from child foreign tables EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM parent_tbl; - QUERY PLAN ------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------- Delete on public.parent_tbl Foreign Delete on public.foreign_tbl parent_tbl_1 - Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 + Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 AND tableoid = $2 -> Foreign Scan on public.foreign_tbl parent_tbl_1 - Output: parent_tbl_1.tableoid, parent_tbl_1.ctid - Remote SQL: SELECT ctid FROM public.local_tbl FOR UPDATE + Output: parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.tableoid, $0 + Remote SQL: SELECT ctid, tableoid FROM public.local_tbl FOR UPDATE (6 rows) DELETE FROM parent_tbl; @@ -8363,39 +8363,41 @@ CREATE TRIGGER parent_tbl_delete_trig FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); EXPLAIN (VERBOSE, COSTS OFF) UPDATE parent_tbl SET b = b + 1; - QUERY PLAN ------------------------------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------- Update on public.parent_tbl Update on public.parent_tbl parent_tbl_1 Foreign Update on public.foreign_tbl parent_tbl_2 - Remote SQL: UPDATE public.local_tbl SET b = $2 WHERE ctid = $1 + Remote SQL: UPDATE public.local_tbl SET b = $3 WHERE ctid = $1 AND tableoid = $2 -> Result - Output: (parent_tbl.b + 1), parent_tbl.tableoid, parent_tbl.ctid, (NULL::record) + Output: (parent_tbl.b + 1), parent_tbl.tableoid, parent_tbl.ctid, (NULL::oid), $0, (NULL::record) -> Append -> Seq Scan on public.parent_tbl parent_tbl_1 - Output: parent_tbl_1.b, parent_tbl_1.tableoid, parent_tbl_1.ctid, NULL::record + Output: parent_tbl_1.b, parent_tbl_1.tableoid, parent_tbl_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.foreign_tbl parent_tbl_2 - Output: parent_tbl_2.b, parent_tbl_2.tableoid, parent_tbl_2.ctid, parent_tbl_2.* - Remote SQL: SELECT a, b, ctid FROM public.local_tbl FOR UPDATE + Output: parent_tbl_2.b, parent_tbl_2.tableoid, parent_tbl_2.ctid, parent_tbl_2.tableoid, parent_tbl_2.*, $0 + Remote SQL: SELECT a, b, ctid, tableoid FROM public.local_tbl FOR UPDATE (12 rows) UPDATE parent_tbl SET b = b + 1; ERROR: cannot collect transition tuples from child foreign tables EXPLAIN (VERBOSE, COSTS OFF) DELETE FROM parent_tbl; - QUERY PLAN ------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------- Delete on public.parent_tbl Delete on public.parent_tbl parent_tbl_1 Foreign Delete on public.foreign_tbl parent_tbl_2 - Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 - -> Append - -> Seq Scan on public.parent_tbl parent_tbl_1 - Output: parent_tbl_1.tableoid, parent_tbl_1.ctid - -> Foreign Scan on public.foreign_tbl parent_tbl_2 - Output: parent_tbl_2.tableoid, parent_tbl_2.ctid - Remote SQL: SELECT ctid FROM public.local_tbl FOR UPDATE -(10 rows) + Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 AND tableoid = $2 + -> Result + Output: parent_tbl.tableoid, parent_tbl.ctid, (NULL::oid), $0 + -> Append + -> Seq Scan on public.parent_tbl parent_tbl_1 + Output: parent_tbl_1.tableoid, parent_tbl_1.ctid, NULL::oid + -> Foreign Scan on public.foreign_tbl parent_tbl_2 + Output: parent_tbl_2.tableoid, parent_tbl_2.ctid, parent_tbl_2.tableoid, $0 + Remote SQL: SELECT ctid, tableoid FROM public.local_tbl FOR UPDATE +(12 rows) DELETE FROM parent_tbl; ERROR: cannot collect transition tuples from child foreign tables @@ -8737,22 +8739,22 @@ drop table foo2child; -- Check UPDATE with inherited target and an inherited source table explain (verbose, costs off) update bar set f2 = f2 + 100 where f1 in (select f1 from foo); - QUERY PLAN -------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------ Update on public.bar Update on public.bar bar_1 Foreign Update on public.bar2 bar_2 - Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1 + Remote SQL: UPDATE public.loct2 SET f2 = $3 WHERE ctid = $1 AND tableoid = $2 -> Hash Join - Output: (bar.f2 + 100), foo.ctid, bar.tableoid, bar.ctid, (NULL::record), foo.*, foo.tableoid + Output: (bar.f2 + 100), foo.ctid, bar.tableoid, bar.ctid, (NULL::oid), $0, (NULL::record), foo.*, foo.tableoid Inner Unique: true Hash Cond: (bar.f1 = foo.f1) -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.bar2 bar_2 - Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, bar_2.* - Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE + Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, bar_2.tableoid, bar_2.*, $0 + Remote SQL: SELECT f1, f2, f3, ctid, tableoid FROM public.loct2 FOR UPDATE -> Hash Output: foo.ctid, foo.f1, foo.*, foo.tableoid -> HashAggregate @@ -8784,24 +8786,24 @@ update bar set f2 = f2 + 100 from ( select f1 from foo union all select f1+3 from foo ) ss where bar.f1 = ss.f1; - QUERY PLAN ------------------------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------------------------------------- Update on public.bar Update on public.bar bar_1 Foreign Update on public.bar2 bar_2 - Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1 + Remote SQL: UPDATE public.loct2 SET f2 = $3 WHERE ctid = $1 AND tableoid = $2 -> Merge Join - Output: (bar.f2 + 100), (ROW(foo.f1)), bar.tableoid, bar.ctid, (NULL::record) + Output: (bar.f2 + 100), (ROW(foo.f1)), bar.tableoid, bar.ctid, (NULL::oid), $0, (NULL::record) Merge Cond: (bar.f1 = foo.f1) -> Sort - Output: bar.f2, bar.f1, bar.tableoid, bar.ctid, (NULL::record) + Output: bar.f2, bar.f1, bar.tableoid, bar.ctid, (NULL::oid), (NULL::record) Sort Key: bar.f1 -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.f2, bar_1.f1, bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.bar2 bar_2 - Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, bar_2.* - Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE + Output: bar_2.f2, bar_2.f1, bar_2.tableoid, bar_2.ctid, bar_2.tableoid, bar_2.*, $0 + Remote SQL: SELECT f1, f2, f3, ctid, tableoid FROM public.loct2 FOR UPDATE -> Sort Output: (ROW(foo.f1)), foo.f1 Sort Key: foo.f1 @@ -8942,19 +8944,21 @@ ERROR: WHERE CURRENT OF is not supported for this table type rollback; explain (verbose, costs off) delete from foo where f1 < 5 returning *; - QUERY PLAN --------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------- Delete on public.foo Output: foo_1.f1, foo_1.f2 Delete on public.foo foo_1 Foreign Delete on public.foo2 foo_2 - -> Append - -> Index Scan using i_foo_f1 on public.foo foo_1 - Output: foo_1.tableoid, foo_1.ctid - Index Cond: (foo_1.f1 < 5) - -> Foreign Delete on public.foo2 foo_2 - Remote SQL: DELETE FROM public.loct1 WHERE ((f1 < 5)) RETURNING f1, f2 -(10 rows) + -> Result + Output: foo.tableoid, foo.ctid, (NULL::oid), $0 + -> Append + -> Index Scan using i_foo_f1 on public.foo foo_1 + Output: foo_1.tableoid, foo_1.ctid, NULL::oid + Index Cond: (foo_1.f1 < 5) + -> Foreign Delete on public.foo2 foo_2 + Remote SQL: DELETE FROM public.loct1 WHERE ((f1 < 5)) RETURNING f1, f2 +(12 rows) delete from foo where f1 < 5 returning *; f1 | f2 @@ -8968,17 +8972,17 @@ delete from foo where f1 < 5 returning *; explain (verbose, costs off) update bar set f2 = f2 + 100 returning *; - QUERY PLAN ------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------- Update on public.bar Output: bar_1.f1, bar_1.f2 Update on public.bar bar_1 Foreign Update on public.bar2 bar_2 -> Result - Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::record) + Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::oid), $0, (NULL::record) -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record -> Foreign Update on public.bar2 bar_2 Remote SQL: UPDATE public.loct2 SET f2 = (f2 + 100) RETURNING f1, f2 (11 rows) @@ -9003,20 +9007,20 @@ AFTER UPDATE OR DELETE ON bar2 FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo'); explain (verbose, costs off) update bar set f2 = f2 + 100; - QUERY PLAN --------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------- Update on public.bar Update on public.bar bar_1 Foreign Update on public.bar2 bar_2 - Remote SQL: UPDATE public.loct2 SET f1 = $2, f2 = $3, f3 = $4 WHERE ctid = $1 RETURNING f1, f2, f3 + Remote SQL: UPDATE public.loct2 SET f1 = $3, f2 = $4, f3 = $5 WHERE ctid = $1 AND tableoid = $2 RETURNING f1, f2, f3 -> Result - Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::record) + Output: (bar.f2 + 100), bar.tableoid, bar.ctid, (NULL::oid), $0, (NULL::record) -> Append -> Seq Scan on public.bar bar_1 - Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::record + Output: bar_1.f2, bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.bar2 bar_2 - Output: bar_2.f2, bar_2.tableoid, bar_2.ctid, bar_2.* - Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE + Output: bar_2.f2, bar_2.tableoid, bar_2.ctid, bar_2.tableoid, bar_2.*, $0 + Remote SQL: SELECT f1, f2, f3, ctid, tableoid FROM public.loct2 FOR UPDATE (12 rows) update bar set f2 = f2 + 100; @@ -9034,20 +9038,22 @@ NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2 NOTICE: OLD: (7,277,77),NEW: (7,377,77) explain (verbose, costs off) delete from bar where f2 < 400; - QUERY PLAN ---------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------- Delete on public.bar Delete on public.bar bar_1 Foreign Delete on public.bar2 bar_2 - Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 RETURNING f1, f2, f3 - -> Append - -> Seq Scan on public.bar bar_1 - Output: bar_1.tableoid, bar_1.ctid, NULL::record - Filter: (bar_1.f2 < 400) - -> Foreign Scan on public.bar2 bar_2 - Output: bar_2.tableoid, bar_2.ctid, bar_2.* - Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE -(11 rows) + Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 AND tableoid = $2 RETURNING f1, f2, f3 + -> Result + Output: bar.tableoid, bar.ctid, (NULL::oid), $0, (NULL::record) + -> Append + -> Seq Scan on public.bar bar_1 + Output: bar_1.tableoid, bar_1.ctid, NULL::oid, NULL::record + Filter: (bar_1.f2 < 400) + -> Foreign Scan on public.bar2 bar_2 + Output: bar_2.tableoid, bar_2.ctid, bar_2.tableoid, bar_2.*, $0 + Remote SQL: SELECT f1, f2, f3, ctid, tableoid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE +(13 rows) delete from bar where f2 < 400; NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON bar2 @@ -9078,22 +9084,22 @@ analyze remt1; analyze remt2; explain (verbose, costs off) update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------- Update on public.parent Output: parent_1.a, parent_1.b, remt2.a, remt2.b Update on public.parent parent_1 Foreign Update on public.remt1 parent_2 - Remote SQL: UPDATE public.loct1 SET b = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.loct1 SET b = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Nested Loop - Output: (parent.b || remt2.b), remt2.*, remt2.a, remt2.b, parent.tableoid, parent.ctid, (NULL::record) + Output: (parent.b || remt2.b), remt2.*, remt2.a, remt2.b, parent.tableoid, parent.ctid, (NULL::oid), $0, (NULL::record) Join Filter: (parent.a = remt2.a) -> Append -> Seq Scan on public.parent parent_1 - Output: parent_1.b, parent_1.a, parent_1.tableoid, parent_1.ctid, NULL::record + Output: parent_1.b, parent_1.a, parent_1.tableoid, parent_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.remt1 parent_2 - Output: parent_2.b, parent_2.a, parent_2.tableoid, parent_2.ctid, parent_2.* - Remote SQL: SELECT a, b, ctid FROM public.loct1 FOR UPDATE + Output: parent_2.b, parent_2.a, parent_2.tableoid, parent_2.ctid, parent_2.tableoid, parent_2.*, $0 + Remote SQL: SELECT a, b, ctid, tableoid FROM public.loct1 FOR UPDATE -> Materialize Output: remt2.b, remt2.*, remt2.a -> Foreign Scan on public.remt2 @@ -9110,22 +9116,22 @@ update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a re explain (verbose, costs off) delete from parent using remt2 where parent.a = remt2.a returning parent; - QUERY PLAN ------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------------------------------------- Delete on public.parent Output: parent_1.* Delete on public.parent parent_1 Foreign Delete on public.remt1 parent_2 - Remote SQL: DELETE FROM public.loct1 WHERE ctid = $1 RETURNING a, b + Remote SQL: DELETE FROM public.loct1 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Nested Loop - Output: remt2.*, parent.tableoid, parent.ctid + Output: remt2.*, parent.tableoid, parent.ctid, (NULL::oid), $0 Join Filter: (parent.a = remt2.a) -> Append -> Seq Scan on public.parent parent_1 - Output: parent_1.a, parent_1.tableoid, parent_1.ctid + Output: parent_1.a, parent_1.tableoid, parent_1.ctid, NULL::oid -> Foreign Scan on public.remt1 parent_2 - Output: parent_2.a, parent_2.tableoid, parent_2.ctid - Remote SQL: SELECT a, ctid FROM public.loct1 FOR UPDATE + Output: parent_2.a, parent_2.tableoid, parent_2.ctid, parent_2.tableoid, $0 + Remote SQL: SELECT a, ctid, tableoid FROM public.loct1 FOR UPDATE -> Materialize Output: remt2.*, remt2.a -> Foreign Scan on public.remt2 @@ -9355,7 +9361,7 @@ update utrtest set a = 1 where a = 1 or a = 2 returning *; -> Foreign Update on public.remp utrtest_1 Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b -> Seq Scan on public.locp utrtest_2 - Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::record + Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::oid, $0, NULL::record Filter: ((utrtest_2.a = 1) OR (utrtest_2.a = 2)) (10 rows) @@ -9394,8 +9400,8 @@ insert into utrtest values (2, 'qux'); -- with a direct modification plan explain (verbose, costs off) update utrtest set a = 1 returning *; - QUERY PLAN ---------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------ Update on public.utrtest Output: utrtest_1.a, utrtest_1.b Foreign Update on public.remp utrtest_1 @@ -9404,7 +9410,7 @@ update utrtest set a = 1 returning *; -> Foreign Update on public.remp utrtest_1 Remote SQL: UPDATE public.loct SET a = 1 RETURNING a, b -> Seq Scan on public.locp utrtest_2 - Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::record + Output: 1, utrtest_2.tableoid, utrtest_2.ctid, NULL::oid, $0, NULL::record (9 rows) update utrtest set a = 1 returning *; @@ -9415,22 +9421,22 @@ insert into utrtest values (2, 'qux'); -- with a non-direct modification plan explain (verbose, costs off) update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *; - QUERY PLAN ------------------------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------- Update on public.utrtest Output: utrtest_1.a, utrtest_1.b, "*VALUES*".column1 Foreign Update on public.remp utrtest_1 - Remote SQL: UPDATE public.loct SET a = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.loct SET a = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b Update on public.locp utrtest_2 -> Hash Join - Output: 1, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, utrtest.* + Output: 1, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, utrtest.tableoid, $0, utrtest.* Hash Cond: (utrtest.a = "*VALUES*".column1) -> Append -> Foreign Scan on public.remp utrtest_1 - Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, utrtest_1.* - Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE + Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, utrtest_1.tableoid, utrtest_1.*, $0 + Remote SQL: SELECT a, b, ctid, tableoid FROM public.loct FOR UPDATE -> Seq Scan on public.locp utrtest_2 - Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, NULL::record + Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, NULL::oid, NULL::record -> Hash Output: "*VALUES*".*, "*VALUES*".column1 -> Values Scan on "*VALUES*" @@ -9454,15 +9460,15 @@ insert into utrtest values (3, 'xyzzy'); -- with a direct modification plan explain (verbose, costs off) update utrtest set a = 3 returning *; - QUERY PLAN ---------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------ Update on public.utrtest Output: utrtest_1.a, utrtest_1.b Update on public.locp utrtest_1 Foreign Update on public.remp utrtest_2 -> Append -> Seq Scan on public.locp utrtest_1 - Output: 3, utrtest_1.tableoid, utrtest_1.ctid, NULL::record + Output: 3, utrtest_1.tableoid, utrtest_1.ctid, NULL::oid, $0, NULL::record -> Foreign Update on public.remp utrtest_2 Remote SQL: UPDATE public.loct SET a = 3 RETURNING a, b (9 rows) @@ -9472,22 +9478,22 @@ ERROR: cannot route tuples into foreign table to be updated "remp" -- with a non-direct modification plan explain (verbose, costs off) update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *; - QUERY PLAN ------------------------------------------------------------------------------------------------------ + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------- Update on public.utrtest Output: utrtest_1.a, utrtest_1.b, "*VALUES*".column1 Update on public.locp utrtest_1 Foreign Update on public.remp utrtest_2 - Remote SQL: UPDATE public.loct SET a = $2 WHERE ctid = $1 RETURNING a, b + Remote SQL: UPDATE public.loct SET a = $3 WHERE ctid = $1 AND tableoid = $2 RETURNING a, b -> Hash Join - Output: 3, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, (NULL::record) + Output: 3, "*VALUES*".*, "*VALUES*".column1, utrtest.tableoid, utrtest.ctid, (NULL::oid), $0, (NULL::record) Hash Cond: (utrtest.a = "*VALUES*".column1) -> Append -> Seq Scan on public.locp utrtest_1 - Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, NULL::record + Output: utrtest_1.a, utrtest_1.tableoid, utrtest_1.ctid, NULL::oid, NULL::record -> Foreign Scan on public.remp utrtest_2 - Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, utrtest_2.* - Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE + Output: utrtest_2.a, utrtest_2.tableoid, utrtest_2.ctid, utrtest_2.tableoid, utrtest_2.*, $0 + Remote SQL: SELECT a, b, ctid, tableoid FROM public.loct FOR UPDATE -> Hash Output: "*VALUES*".*, "*VALUES*".column1 -> Values Scan on "*VALUES*" @@ -12374,8 +12380,8 @@ RESET enable_hashjoin; -- Test that UPDATE/DELETE with inherited target works with async_capable enabled EXPLAIN (VERBOSE, COSTS OFF) UPDATE async_pt SET c = c || c WHERE b = 0 RETURNING *; - QUERY PLAN ----------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------- Update on public.async_pt Output: async_pt_1.a, async_pt_1.b, async_pt_1.c Foreign Update on public.async_p1 async_pt_1 @@ -12387,7 +12393,7 @@ UPDATE async_pt SET c = c || c WHERE b = 0 RETURNING *; -> Foreign Update on public.async_p2 async_pt_2 Remote SQL: UPDATE public.base_tbl2 SET c = (c || c) WHERE ((b = 0)) RETURNING a, b, c -> Seq Scan on public.async_p3 async_pt_3 - Output: (async_pt_3.c || async_pt_3.c), async_pt_3.tableoid, async_pt_3.ctid, NULL::record + Output: (async_pt_3.c || async_pt_3.c), async_pt_3.tableoid, async_pt_3.ctid, NULL::oid, $0, NULL::record, $1 Filter: (async_pt_3.b = 0) (13 rows) @@ -12414,7 +12420,7 @@ DELETE FROM async_pt WHERE b = 0 RETURNING *; -> Foreign Delete on public.async_p2 async_pt_2 Remote SQL: DELETE FROM public.base_tbl2 WHERE ((b = 0)) RETURNING a, b, c -> Seq Scan on public.async_p3 async_pt_3 - Output: async_pt_3.tableoid, async_pt_3.ctid + Output: async_pt_3.tableoid, async_pt_3.ctid, NULL::oid, $0, $1 Filter: (async_pt_3.b = 0) (13 rows) @@ -12977,3 +12983,49 @@ SELECT server_name, -- Clean up \set VERBOSITY default RESET debug_discard_caches; +-- =================================================================== +-- check whether fdw created for partitioned table will delete tuples only from +-- desired partition +-- =================================================================== +CREATE TABLE measurement ( + city_id int not null, + logdate date not null, + peaktemp int, + unitsales int +) PARTITION BY RANGE (logdate); +CREATE TABLE measurement_y2006m02 PARTITION OF measurement + FOR VALUES FROM ('2006-02-01') TO ('2006-03-01'); +CREATE TABLE measurement_y2006m03 PARTITION OF measurement + FOR VALUES FROM ('2006-03-01') TO ('2006-04-01'); +CREATE TABLE measurement_y2006m04 PARTITION OF measurement + FOR VALUES FROM ('2006-04-01') TO ('2006-05-01'); +INSERT INTO measurement VALUES (1,'2006-02-01',1,1); +INSERT INTO measurement VALUES (2,'2006-03-01',1,1); +INSERT INTO measurement VALUES (3,'2006-04-01',1,1); +create foreign table measurement_fdw ( + city_id int options (column_name 'city_id') not null, + logdate date options (column_name 'logdate') not null, + peaktemp text options (column_name 'peaktemp'), + unitsales integer options (column_name 'unitsales') +) SERVER loopback OPTIONS (table_name 'measurement'); +DELETE FROM measurement_fdw +USING ( + SELECT t1.city_id sub_city_id + FROM measurement_fdw t1 + WHERE t1.city_id=1 + LIMIT 1000 +) sub +WHERE measurement_fdw.city_id = sub.sub_city_id +RETURNING city_id, logdate, peaktemp, unitsales; + city_id | logdate | peaktemp | unitsales +---------+------------+----------+----------- + 1 | 02-01-2006 | 1 | 1 +(1 row) + +SELECT * FROM measurement_fdw; + city_id | logdate | peaktemp | unitsales +---------+------------+----------+----------- + 2 | 03-01-2006 | 1 | 1 + 3 | 04-01-2006 | 1 | 1 +(2 rows) + diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 0f20f38c83e..45079999ecf 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -80,6 +80,8 @@ enum FdwScanPrivateIndex FdwScanPrivateSelectSql, /* Integer list of attribute numbers retrieved by the SELECT */ FdwScanPrivateRetrievedAttrs, + /* Param ID for remote table OID for target rel (-1 if none) */ + FdwScanPrivateTableOidParamId, /* Integer representing the desired fetch_size */ FdwScanPrivateFetchSize, @@ -178,6 +180,10 @@ typedef struct PgFdwScanState MemoryContext temp_cxt; /* context for per-tuple temporary data */ int fetch_size; /* number of tuples per fetch */ + + int tableoid_param_id; /* Param ID for remote table OID */ + bool set_tableoid_param; /* Do we need to set the Param? */ + Oid remote_tableoid; } PgFdwScanState; /* @@ -204,6 +210,7 @@ typedef struct PgFdwModifyState /* info about parameters for prepared statement */ AttrNumber ctidAttno; /* attnum of input resjunk ctid column */ + AttrNumber tableoidAttno; /* attnum of input resjunk tableoid column */ int p_nums; /* number of parameters to transmit */ FmgrInfo *p_flinfo; /* output conversion functions for them */ @@ -304,6 +311,20 @@ typedef struct int64 offset_est; } PgFdwPathExtraData; +typedef struct OidMappingEntry +{ + Oid key; + Oid remote_oid; +} OidMappingEntry; + +typedef struct PgFdwRemoteMap +{ + HTAB *oid_mapping; + uint32 refcount; +} PgFdwRemoteMap; + +static PgFdwRemoteMap *remoteMap; + /* * Identify the attribute where data conversion fails. */ @@ -656,6 +677,7 @@ static TupleTableSlot **execute_foreign_modify(EState *estate, static void prepare_foreign_modify(PgFdwModifyState *fmstate); static const char **convert_prep_stmt_params(PgFdwModifyState *fmstate, ItemPointer tupleid, + Oid tableoid, TupleTableSlot **slots, int numSlots); static void store_returning_result(PgFdwModifyState *fmstate, @@ -760,6 +782,88 @@ static void merge_fdw_options(PgFdwRelationInfo *fpinfo, const PgFdwRelationInfo *fpinfo_i); static int get_batch_size_option(Relation rel); +/* Private cache */ +static void +init_remote_map(void) +{ + MemoryContext hash_cxt; + HASHCTL info; + + Assert(remoteMap == NULL); + + remoteMap = (PgFdwRemoteMap *) + MemoryContextAllocZero(CacheMemoryContext, sizeof(PgFdwRemoteMap)); + + hash_cxt = AllocSetContextCreate(CacheMemoryContext, + "PgFdwRemoteMap", + ALLOCSET_DEFAULT_SIZES); + info.keysize = sizeof(Oid); + info.entrysize = sizeof(OidMappingEntry); + info.hcxt = hash_cxt; + + remoteMap->oid_mapping = + hash_create("PgFdwRemoteMap hashtable", + 256, /* arbitrary initial size */ + &info, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +static void +insert_oid_pair(Oid local_oid, Oid remote_oid) +{ + bool found; + OidMappingEntry *entry; + + Assert(OidIsValid(local_oid) && OidIsValid(remote_oid)); + + if (remoteMap == NULL) + init_remote_map(); + + /* This mapping may already present in the hashtable. */ + entry = (OidMappingEntry *) + hash_search(remoteMap->oid_mapping, &local_oid, HASH_ENTER, &found); + + if (!found) + { + remoteMap->refcount++; + entry->remote_oid = remote_oid; + } +} + +static Oid +find_remote_oid(Oid local_oid) +{ + OidMappingEntry *entry; + bool found; + + Assert(OidIsValid(local_oid)); + + if (remoteMap == NULL) + return InvalidOid; + + entry = (OidMappingEntry *) + hash_search(remoteMap->oid_mapping, &local_oid, HASH_FIND, &found); + + return (found ? entry->remote_oid : InvalidOid); +} + +static void +destroy_remote_map(void) +{ + if (remoteMap == NULL) + return; + + Assert(remoteMap->refcount > 0); + remoteMap->refcount--; + + if (remoteMap->refcount == 0) + { + hash_destroy(remoteMap->oid_mapping); + + pfree(remoteMap); + remoteMap = NULL; + } +} /* * Foreign-data wrapper handler function: return a struct with pointers @@ -1002,6 +1106,23 @@ postgresGetForeignRelSize(PlannerInfo *root, fpinfo->hidden_subquery_rels = NULL; /* Set the relation index. */ fpinfo->relation_index = baserel->relid; + fpinfo->tableoid_param = NULL; + + /* + * If the table is an UPDATE/DELETE target, the table's reltarget would + * have contained a Param representing the remote table OID of the target; + * get the Param and save a copy of it in fpinfo for use later. + */ + foreach(lc, baserel->reltarget->exprs) + { + Param *param = (Param *) lfirst(lc); + if (IsA(param, Param)) + { + Assert(IS_FOREIGN_PARAM(root, param)); + fpinfo->tableoid_param = (Param *) copyObject(param); + break; + } + } } /* @@ -1463,6 +1584,7 @@ postgresGetForeignPlan(PlannerInfo *root, bool has_final_sort = false; bool has_limit = false; ListCell *lc; + int tableoid_param_id = -1; /* * Get FDW private data created by postgresGetForeignUpperPaths(), if any. @@ -1627,12 +1749,16 @@ postgresGetForeignPlan(PlannerInfo *root, /* Remember remote_exprs for possible use by postgresPlanDirectModify */ fpinfo->final_remote_exprs = remote_exprs; + if (fpinfo->tableoid_param) + tableoid_param_id = fpinfo->tableoid_param->paramid; + /* * Build the fdw_private list that will be available to the executor. * Items in the list must match order in enum FdwScanPrivateIndex. */ - fdw_private = list_make3(makeString(sql.data), + fdw_private = list_make4(makeString(sql.data), retrieved_attrs, + makeInteger(tableoid_param_id), makeInteger(fpinfo->fetch_size)); if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel)) fdw_private = lappend(fdw_private, @@ -1765,6 +1891,8 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) FdwScanPrivateSelectSql)); fsstate->retrieved_attrs = (List *) list_nth(fsplan->fdw_private, FdwScanPrivateRetrievedAttrs); + fsstate->tableoid_param_id = intVal(list_nth(fsplan->fdw_private, + FdwScanPrivateTableOidParamId)); fsstate->fetch_size = intVal(list_nth(fsplan->fdw_private, FdwScanPrivateFetchSize)); @@ -1784,11 +1912,13 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) { fsstate->rel = node->ss.ss_currentRelation; fsstate->tupdesc = RelationGetDescr(fsstate->rel); + fsstate->set_tableoid_param = (fsstate->tableoid_param_id >= 0); } else { fsstate->rel = NULL; fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node); + fsstate->set_tableoid_param = false; } fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc); @@ -1820,6 +1950,7 @@ postgresIterateForeignScan(ForeignScanState *node) { PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state; TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; + HeapTuple tuple; /* * In sync mode, if this is the first call after Begin or ReScan, we need @@ -1846,12 +1977,23 @@ postgresIterateForeignScan(ForeignScanState *node) return ExecClearTuple(slot); } + tuple = fsstate->tuples[fsstate->next_tuple++]; + + if (fsstate->set_tableoid_param) + { + ExprContext *econtext = node->ss.ps.ps_ExprContext; + ParamExecData *prm = &(econtext->ecxt_param_exec_vals[fsstate->tableoid_param_id]); + + prm->execPlan = NULL; + prm->value = ObjectIdGetDatum(tuple->t_tableOid); + prm->isnull = false; + + insert_oid_pair(RelationGetRelid(fsstate->rel), tuple->t_tableOid); + } /* * Return the next tuple. */ - ExecStoreHeapTuple(fsstate->tuples[fsstate->next_tuple++], - slot, - false); + ExecStoreHeapTuple(tuple, slot, false); return slot; } @@ -1966,6 +2108,9 @@ postgresAddForeignUpdateTargets(PlannerInfo *root, Relation target_relation) { Var *var; + Param *param; + const char *attrname; + TargetEntry *tle; /* * In postgres_fdw, what we need is the ctid, same as for a regular table. @@ -1981,6 +2126,39 @@ postgresAddForeignUpdateTargets(PlannerInfo *root, /* Register it as a row-identity column needed by this target rel */ add_row_identity_var(root, var, rtindex, "ctid"); + + /* Make a Var representing the desired value */ + var = makeVar(rtindex, + TableOidAttributeNumber, + OIDOID, + -1, + InvalidOid, + 0); + /* Register it as a row-identity column needed by this target rel */ + add_row_identity_var(root, var, rtindex, "remote_tableoid"); + + /* Make a Param representing the tableoid value */ + param = makeNode(Param); + param->paramkind = PARAM_EXEC; + param->paramtype = OIDOID; + param->paramtypmod = -1; + param->paramcollid = InvalidOid; + param->location = -1; + /* paramid will be filled in by fix_foreign_params */ + param->paramid = -1; + param->target_rte = rtindex; + + /* Wrap it in a resjunk TLE with the right name ... */ + + attrname = "remote_tableoid"; + + tle = makeTargetEntry((Expr *) param, + list_length(root->processed_tlist) + 1, + pstrdup(attrname), + true); + + /* ... and add it to the query's targetlist */ + root->processed_tlist = lappend(root->processed_tlist, tle); } /* @@ -2343,6 +2521,7 @@ postgresExecForeignDelete(EState *estate, rslot = execute_foreign_modify(estate, resultRelInfo, CMD_DELETE, &slot, &planSlot, &numSlots); + destroy_remote_map(); return rslot ? rslot[0] : NULL; } @@ -4228,7 +4407,7 @@ create_foreign_modify(EState *estate, fmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc); /* Prepare for output conversion of parameters used in prepared stmt. */ - n_params = list_length(fmstate->target_attrs) + 1; + n_params = list_length(fmstate->target_attrs) + 2; fmstate->p_flinfo = palloc0_array(FmgrInfo, n_params); fmstate->p_nums = 0; @@ -4246,6 +4425,20 @@ create_foreign_modify(EState *estate, getTypeOutputInfo(TIDOID, &typefnoid, &isvarlena); fmgr_info(typefnoid, &fmstate->p_flinfo[fmstate->p_nums]); fmstate->p_nums++; + + /* Find the tableoid resjunk column in the subplan's result */ + fmstate->tableoidAttno = ExecFindJunkAttributeInTlist(subplan->targetlist, + "remote_tableoid"); + + if (!AttributeNumberIsValid(fmstate->tableoidAttno)) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not find junk tableoid column"))); + + /* Second transmittable parameter will be tableoid */ + getTypeOutputInfo(OIDOID, &typefnoid, &isvarlena); + fmgr_info(typefnoid, &fmstate->p_flinfo[fmstate->p_nums]); + fmstate->p_nums++; } if (operation == CMD_INSERT || operation == CMD_UPDATE) @@ -4298,6 +4491,7 @@ execute_foreign_modify(EState *estate, { PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState; ItemPointer ctid = NULL; + Oid tableoid = InvalidOid; const char **p_values; PGresult *res; int n_rows; @@ -4343,7 +4537,9 @@ execute_foreign_modify(EState *estate, if (operation == CMD_UPDATE || operation == CMD_DELETE) { Datum datum; + Datum datum2; bool isNull; + Oid remote_tableoid = InvalidOid; datum = ExecGetJunkAttribute(planSlots[0], fmstate->ctidAttno, @@ -4352,10 +4548,28 @@ execute_foreign_modify(EState *estate, if (isNull) elog(ERROR, "ctid is NULL"); ctid = (ItemPointer) DatumGetPointer(datum); + + /* Get the tableoid that was passed up as a resjunk column */ + datum2 = ExecGetJunkAttribute(planSlots[0], + fmstate->tableoidAttno, + &isNull); + /* shouldn't ever get a null result... */ + if (isNull) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("tableoid is NULL"))); + + tableoid = DatumGetObjectId(datum2); + + /* Remote OID */ + remote_tableoid = find_remote_oid(tableoid); + + if(OidIsValid(remote_tableoid)) + tableoid = remote_tableoid; } /* Convert parameters needed by prepared statement to text form */ - p_values = convert_prep_stmt_params(fmstate, ctid, slots, *numSlots); + p_values = convert_prep_stmt_params(fmstate, ctid, tableoid, slots, *numSlots); /* * Execute the prepared statement. @@ -4460,6 +4674,7 @@ prepare_foreign_modify(PgFdwModifyState *fmstate) static const char ** convert_prep_stmt_params(PgFdwModifyState *fmstate, ItemPointer tupleid, + Oid tableoid, TupleTableSlot **slots, int numSlots) { @@ -4486,6 +4701,16 @@ convert_prep_stmt_params(PgFdwModifyState *fmstate, pindex++; } + /* 2nd parameter should be tableoid, if it's in use */ + if (OidIsValid(tableoid)) + { + Assert(tupleid != NULL); + /* don't need set_transmission_modes for OID output */ + p_values[pindex] = OutputFunctionCall(&fmstate->p_flinfo[pindex], + ObjectIdGetDatum(tableoid)); + pindex++; + } + /* get following parameters from slots */ if (slots != NULL && fmstate->target_attrs != NIL) { @@ -4497,7 +4722,7 @@ convert_prep_stmt_params(PgFdwModifyState *fmstate, for (i = 0; i < numSlots; i++) { - j = (tupleid != NULL) ? 1 : 0; + j = (tupleid != NULL) ? 2 : 0; foreach(lc, fmstate->target_attrs) { int attnum = lfirst_int(lc); @@ -4562,9 +4787,10 @@ finish_foreign_modify(PgFdwModifyState *fmstate) { Assert(fmstate != NULL); + destroy_remote_map(); + /* If we created a prepared statement, destroy it */ deallocate_query(fmstate); - /* Release remote connection */ ReleaseConnection(fmstate->conn); fmstate->conn = NULL; @@ -4670,7 +4896,8 @@ build_remote_returning(Index rtindex, Relation rel, List *returningList) if (IsA(var, Var) && var->varno == rtindex && var->varattno <= InvalidAttrNumber && - var->varattno != SelfItemPointerAttributeNumber) + var->varattno != SelfItemPointerAttributeNumber && + var->varattno != TableOidAttributeNumber) continue; /* don't need it */ if (tlist_member((Expr *) var, tlist)) @@ -4876,6 +5103,17 @@ init_returning_filter(PgFdwDirectModifyState *dmstate, TargetEntry *tle = (TargetEntry *) lfirst(lc); Var *var = (Var *) tle->expr; + /* + * No need to set the Param for the remote table OID; ignore it. + */ + if (IsA(var, Param)) + { + /* We would not retrieve the remote table OID anymore. */ + Assert(!list_member_int(dmstate->retrieved_attrs, i)); + i++; + continue; + } + Assert(IsA(var, Var)); /* @@ -4894,6 +5132,8 @@ init_returning_filter(PgFdwDirectModifyState *dmstate, */ if (attrno == SelfItemPointerAttributeNumber) dmstate->ctidAttno = i; + else if (attrno == TableOidAttributeNumber) + dmstate->oidAttno = i; else Assert(false); dmstate->hasSystemCols = true; @@ -4985,10 +5225,13 @@ apply_returning_filter(PgFdwDirectModifyState *dmstate, /* ctid */ if (dmstate->ctidAttno) { + Oid tableoid = InvalidOid; ItemPointer ctid = NULL; ctid = (ItemPointer) DatumGetPointer(old_values[dmstate->ctidAttno - 1]); + tableoid = DatumGetObjectId(old_values[dmstate->oidAttno - 1]); resultTup->t_self = *ctid; + resultTup->t_tableOid = tableoid; } /* @@ -6901,6 +7144,38 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, /* Mark that this join can be pushed down safely */ fpinfo->pushdown_safe = true; + /* + * If the join relation contains an UPDATE/DELETE target, either of the + * input relations would have saved the Param representing the remote + * table OID of the target; get the Param and remember it in fpinfo for + * use later. + */ + if ((root->parse->commandType == CMD_UPDATE || + root->parse->commandType == CMD_DELETE) && + bms_is_member(root->parse->resultRelation, joinrel->relids)) + { + if (bms_is_member(root->parse->resultRelation, + outerrel->relids)) + { + Assert(fpinfo_o->tableoid_param); + fpinfo->tableoid_param = fpinfo_o->tableoid_param; + } + else + { + Assert(bms_is_member(root->parse->resultRelation, + innerrel->relids)); + Assert(fpinfo_i->tableoid_param); + fpinfo->tableoid_param = fpinfo_i->tableoid_param; + } + /* + * Core code should have contained the Param in the join relation's + * reltarget. + */ + Assert(list_member(joinrel->reltarget->exprs, fpinfo->tableoid_param)); + } + else + fpinfo->tableoid_param = NULL; + /* Get user mapping */ if (fpinfo->use_remote_estimate) { @@ -8434,6 +8709,7 @@ make_tuple_from_result_row(PGresult *res, ErrorContextCallback errcallback; MemoryContext oldcontext; ListCell *lc; + Oid tableoid = InvalidOid; int j; Assert(row < PQntuples(res)); @@ -8516,6 +8792,17 @@ make_tuple_from_result_row(PGresult *res, ctid = (ItemPointer) DatumGetPointer(datum); } } + else if (i == TableOidAttributeNumber) + { + /* tableoid */ + if (valstr != NULL) + { + Datum datum; + + datum = DirectFunctionCall1(oidin, CStringGetDatum(valstr)); + tableoid = DatumGetObjectId(datum); + } + } errpos.cur_attno = 0; j++; @@ -8547,6 +8834,9 @@ make_tuple_from_result_row(PGresult *res, if (ctid) tuple->t_self = tuple->t_data->t_ctid = *ctid; +// if (OidIsValid(tableoid)) + tuple->t_tableOid = tableoid; + /* * Stomp on the xmin, xmax, and cmin fields from the tuple created by * heap_form_tuple. heap_form_tuple actually creates the tuple with diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index a2bb1ff352c..376c55d4dd0 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -129,6 +129,9 @@ typedef struct PgFdwRelationInfo * representing the relation. */ int relation_index; + + /* PARAM_EXEC Param representing the remote table OID of a target rel */ + Param *tableoid_param; } PgFdwRelationInfo; /* diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 79ad5be8bf9..81d7cf03c1a 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -4620,3 +4620,47 @@ SELECT server_name, -- Clean up \set VERBOSITY default RESET debug_discard_caches; + +-- =================================================================== +-- check whether fdw created for partitioned table will delete tuples only from +-- desired partition +-- =================================================================== + +CREATE TABLE measurement ( + city_id int not null, + logdate date not null, + peaktemp int, + unitsales int +) PARTITION BY RANGE (logdate); + +CREATE TABLE measurement_y2006m02 PARTITION OF measurement + FOR VALUES FROM ('2006-02-01') TO ('2006-03-01'); + +CREATE TABLE measurement_y2006m03 PARTITION OF measurement + FOR VALUES FROM ('2006-03-01') TO ('2006-04-01'); + +CREATE TABLE measurement_y2006m04 PARTITION OF measurement + FOR VALUES FROM ('2006-04-01') TO ('2006-05-01'); + +INSERT INTO measurement VALUES (1,'2006-02-01',1,1); +INSERT INTO measurement VALUES (2,'2006-03-01',1,1); +INSERT INTO measurement VALUES (3,'2006-04-01',1,1); + +create foreign table measurement_fdw ( + city_id int options (column_name 'city_id') not null, + logdate date options (column_name 'logdate') not null, + peaktemp text options (column_name 'peaktemp'), + unitsales integer options (column_name 'unitsales') +) SERVER loopback OPTIONS (table_name 'measurement'); + +DELETE FROM measurement_fdw +USING ( + SELECT t1.city_id sub_city_id + FROM measurement_fdw t1 + WHERE t1.city_id=1 + LIMIT 1000 +) sub +WHERE measurement_fdw.city_id = sub.sub_city_id +RETURNING city_id, logdate, peaktemp, unitsales; + +SELECT * FROM measurement_fdw; -- 2.43.0 [application/octet-stream] v1-0001-copy-slot-tuple-natts.patch (29.9K, ../../CAN-LCVMz58ukZ7ubGXiLuTeFE7wWmSwDw4URpF0q1ejzRvqbzg@mail.gmail.com/4-v1-0001-copy-slot-tuple-natts.patch) download | inline diff: From 864d2e675dd531c7cd65cc16adef9fc830994957 Mon Sep 17 00:00:00 2001 From: Nikita Malakhov <[email protected]> Date: Tue, 21 Apr 2026 14:01:19 +0300 Subject: [PATCH] [POC] Current copyTuple and copySlot machinery supposes that source and destination slots and tuples always match. This patch modifies copy internals to consider more complex case and copy only actual attributes, and if destination slot/tuple has more they are set to null. This is a proof of concept patch which shows such mechanics does not break anything and allows to use it in cases when we have to pass or receive not matching number of attributes. --- src/backend/access/common/heaptuple.c | 195 ++++++++++++++++++++----- src/backend/executor/execTuples.c | 196 +++++++++++++++++++++++--- src/backend/optimizer/util/tlist.c | 39 +++-- src/include/access/htup_details.h | 14 ++ src/include/executor/tuptable.h | 47 ++++++ 5 files changed, 424 insertions(+), 67 deletions(-) diff --git a/src/backend/access/common/heaptuple.c b/src/backend/access/common/heaptuple.c index f30346469ed..ff4bdda0327 100644 --- a/src/backend/access/common/heaptuple.c +++ b/src/backend/access/common/heaptuple.c @@ -58,9 +58,12 @@ #include "postgres.h" #include "access/heaptoast.h" +#include "access/htup.h" +#include "access/htup_details.h" #include "access/sysattr.h" #include "access/tupdesc_details.h" #include "common/hashfn.h" +#include "executor/tuptable.h" #include "utils/datum.h" #include "utils/expandeddatum.h" #include "utils/hsearch.h" @@ -212,17 +215,18 @@ getmissingattr(TupleDesc tupleDesc, } /* - * heap_compute_data_size + * heap_compute_data_size_ext * Determine size of the data area of a tuple to be constructed */ Size -heap_compute_data_size(TupleDesc tupleDesc, +heap_compute_data_size_ext(TupleDesc tupleDesc, const Datum *values, - const bool *isnull) + const bool *isnull, + int attnum) { Size data_length = 0; int i; - int numberOfAttributes = tupleDesc->natts; + int numberOfAttributes = (attnum >= 0 ? attnum : tupleDesc->natts); for (i = 0; i < numberOfAttributes; i++) { @@ -266,6 +270,18 @@ heap_compute_data_size(TupleDesc tupleDesc, return data_length; } +/* + * heap_compute_data_size + * Determine size of the data area of a tuple to be constructed + */ +Size +heap_compute_data_size(TupleDesc tupleDesc, + const Datum *values, + const bool *isnull) +{ + return heap_compute_data_size_ext(tupleDesc, values, isnull, -1); +} + /* * Per-attribute helper for heap_fill_tuple and other routines building tuples. * @@ -398,10 +414,10 @@ fill_val(CompactAttribute *att, * NOTE: it is now REQUIRED that the caller have pre-zeroed the data area. */ void -heap_fill_tuple(TupleDesc tupleDesc, +heap_fill_tuple_ext(TupleDesc tupleDesc, const Datum *values, const bool *isnull, char *data, Size data_size, - uint16 *infomask, uint8 *bit) + uint16 *infomask, uint8 *bit, int natts) { uint8 *bitP; int bitmask; @@ -412,6 +428,9 @@ heap_fill_tuple(TupleDesc tupleDesc, char *start = data; #endif + if(natts > 0) + numberOfAttributes = natts; + if (bit != NULL) { bitP = &bit[-1]; @@ -442,6 +461,25 @@ heap_fill_tuple(TupleDesc tupleDesc, Assert((data - start) == data_size); } +/* + * heap_fill_tuple + * Load data portion of a tuple from values/isnull arrays + * + * We also fill the null bitmap (if any) and set the infomask bits + * that reflect the tuple's data contents. + * + * NOTE: it is now REQUIRED that the caller have pre-zeroed the data area. + */ +void +heap_fill_tuple(TupleDesc tupleDesc, + const Datum *values, const bool *isnull, + char *data, Size data_size, + uint16 *infomask, uint8 *bit) +{ + heap_fill_tuple_ext(tupleDesc, values, isnull, + data, data_size, infomask, bit, -1); +} + /* ---------------------------------------------------------------- * heap tuple interface @@ -685,6 +723,9 @@ heap_getsysattr(HeapTuple tup, int attnum, TupleDesc tupleDesc, bool *isnull) HeapTuple heap_copytuple(HeapTuple tuple) { + return heap_copytuple_ext(tuple, NULL, -1); + + /* HeapTuple newTuple; if (!HeapTupleIsValid(tuple) || tuple->t_data == NULL) @@ -697,13 +738,53 @@ heap_copytuple(HeapTuple tuple) newTuple->t_data = (HeapTupleHeader) ((char *) newTuple + HEAPTUPLESIZE); memcpy(newTuple->t_data, tuple->t_data, tuple->t_len); return newTuple; + */ +} + +/* ---------------- + * heap_copytuple_ext + * + * returns a copy of a tuple consists of attnum atts of original + * + * The HeapTuple struct, tuple header, and tuple data are all allocated + * as a single palloc() block. + * ---------------- + */ +HeapTuple +heap_copytuple_ext(HeapTuple tuple, void *slotptr, int dstnatts) +{ + Size tsz = 0; + HeapTuple newTuple; + TupleTableSlot *slot = NULL; + + if (!HeapTupleIsValid(tuple) || tuple->t_data == NULL) + return NULL; + + tsz = tuple->t_len; + if(slotptr != NULL) + { + slot = (TupleTableSlot *) slotptr; + if(dstnatts <= 0) + dstnatts = slot->tts_tupleDescriptor->natts; + + if(dstnatts < slot->tts_tupleDescriptor->natts) + tsz = heap_compute_data_size_ext(slot->tts_tupleDescriptor, slot->tts_values, slot->tts_isnull, dstnatts); + } + newTuple = (HeapTuple) palloc(HEAPTUPLESIZE + tsz); + newTuple->t_len = tsz; + newTuple->t_self = tuple->t_self; + newTuple->t_tableOid = tuple->t_tableOid; + + newTuple->t_data = (HeapTupleHeader) ((char *) newTuple + HEAPTUPLESIZE); + memcpy((char *) newTuple->t_data, (char *) tuple->t_data, tsz); + return newTuple; } /* ---------------- * heap_copytuple_with_tuple * * copy a tuple into a caller-supplied HeapTuple management struct - * + * * Note that after calling this function, the "dest" HeapTuple will not be * allocated as a single palloc() block (unlike with heap_copytuple()). * ---------------- @@ -1014,17 +1095,11 @@ heap_copy_tuple_as_datum(HeapTuple tuple, TupleDesc tupleDesc) return PointerGetDatum(td); } -/* - * heap_form_tuple - * construct a tuple from the given values[] and isnull[] arrays, - * which are of the length indicated by tupleDescriptor->natts - * - * The result is allocated in the current memory context. - */ HeapTuple -heap_form_tuple(TupleDesc tupleDescriptor, +heap_form_tuple_ext(TupleDesc tupleDescriptor, const Datum *values, - const bool *isnull) + const bool *isnull, + int natts) { HeapTuple tuple; /* return tuple */ HeapTupleHeader td; /* tuple data */ @@ -1035,6 +1110,9 @@ heap_form_tuple(TupleDesc tupleDescriptor, int numberOfAttributes = tupleDescriptor->natts; int i; + if(natts > 0) + numberOfAttributes = natts; + if (numberOfAttributes > MaxTupleAttributeNumber) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_COLUMNS), @@ -1063,7 +1141,10 @@ heap_form_tuple(TupleDesc tupleDescriptor, hoff = len = MAXALIGN(len); /* align user data safely */ - data_len = heap_compute_data_size(tupleDescriptor, values, isnull); + if(natts > 0) + data_len = heap_compute_data_size_ext(tupleDescriptor, values, isnull, natts); + else + data_len = heap_compute_data_size(tupleDescriptor, values, isnull); len += data_len; @@ -1092,7 +1173,17 @@ heap_form_tuple(TupleDesc tupleDescriptor, HeapTupleHeaderSetNatts(td, numberOfAttributes); td->t_hoff = hoff; - heap_fill_tuple(tupleDescriptor, + if(natts > 0) + heap_fill_tuple_ext(tupleDescriptor, + values, + isnull, + (char *) td + hoff, + data_len, + &td->t_infomask, + (hasnull ? td->t_bits : NULL), + natts); + else + heap_fill_tuple(tupleDescriptor, values, isnull, (char *) td + hoff, @@ -1103,6 +1194,24 @@ heap_form_tuple(TupleDesc tupleDescriptor, return tuple; } +/* + * heap_form_tuple + * construct a tuple from the given values[] and isnull[] arrays, + * which are of the length indicated by tupleDescriptor->natts + * + * The result is allocated in the current memory context. + */ +HeapTuple +heap_form_tuple(TupleDesc tupleDescriptor, + const Datum *values, + const bool *isnull) +{ + return heap_form_tuple_ext(tupleDescriptor, + values, + isnull, + -1); +} + /* * heap_modify_tuple * form a new tuple from an old tuple and a set of replacement values. @@ -1375,22 +1484,12 @@ heap_freetuple(HeapTuple htup) } -/* - * heap_form_minimal_tuple - * construct a MinimalTuple from the given values[] and isnull[] arrays, - * which are of the length indicated by tupleDescriptor->natts - * - * This is exactly like heap_form_tuple() except that the result is a - * "minimal" tuple lacking a HeapTupleData header as well as room for system - * columns. - * - * The result is allocated in the current memory context. - */ MinimalTuple -heap_form_minimal_tuple(TupleDesc tupleDescriptor, +heap_form_minimal_tuple_ext(TupleDesc tupleDescriptor, const Datum *values, const bool *isnull, - Size extra) + Size extra, + int natts) { MinimalTuple tuple; /* return tuple */ char *mem; @@ -1403,6 +1502,9 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor, Assert(extra == MAXALIGN(extra)); + if(natts > 0) + numberOfAttributes = natts; + if (numberOfAttributes > MaxTupleAttributeNumber) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_COLUMNS), @@ -1431,7 +1533,7 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor, hoff = len = MAXALIGN(len); /* align user data safely */ - data_len = heap_compute_data_size(tupleDescriptor, values, isnull); + data_len = heap_compute_data_size_ext(tupleDescriptor, values, isnull, natts); len += data_len; @@ -1448,17 +1550,42 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor, HeapTupleHeaderSetNatts(tuple, numberOfAttributes); tuple->t_hoff = hoff + MINIMAL_TUPLE_OFFSET; - heap_fill_tuple(tupleDescriptor, + heap_fill_tuple_ext(tupleDescriptor, values, isnull, (char *) tuple + hoff, data_len, &tuple->t_infomask, - (hasnull ? tuple->t_bits : NULL)); + (hasnull ? tuple->t_bits : NULL), + natts); return tuple; } +/* + * heap_form_minimal_tuple + * construct a MinimalTuple from the given values[] and isnull[] arrays, + * which are of the length indicated by tupleDescriptor->natts + * + * This is exactly like heap_form_tuple() except that the result is a + * "minimal" tuple lacking a HeapTupleData header as well as room for system + * columns. + * + * The result is allocated in the current memory context. + */ +MinimalTuple +heap_form_minimal_tuple(TupleDesc tupleDescriptor, + const Datum *values, + const bool *isnull, + Size extra) +{ + return heap_form_minimal_tuple_ext(tupleDescriptor, + values, + isnull, + extra, + -1); +} + /* * heap_free_minimal_tuple */ diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c index f08982a43cc..c98b49486b0 100644 --- a/src/backend/executor/execTuples.c +++ b/src/backend/executor/execTuples.c @@ -80,6 +80,7 @@ static inline void tts_buffer_heap_store_tuple(TupleTableSlot *slot, bool transfer_pin); static void tts_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple, bool shouldFree); +static void tts_heap_materialize_ext(TupleTableSlot *slot, int natts); const TupleTableSlotOps TTSOpsVirtual; const TupleTableSlotOps TTSOpsHeapTuple; @@ -269,24 +270,61 @@ static void tts_virtual_copyslot(TupleTableSlot *dstslot, TupleTableSlot *srcslot) { TupleDesc srcdesc = srcslot->tts_tupleDescriptor; + TupleDesc dstdesc = dstslot->tts_tupleDescriptor; + int attnum = srcdesc->natts; tts_virtual_clear(dstslot); slot_getallattrs(srcslot); - for (int natt = 0; natt < srcdesc->natts; natt++) + if(dstdesc->natts < srcdesc->natts) + attnum = dstdesc->natts; + + for (int natt = 0; natt < attnum; natt++) { dstslot->tts_values[natt] = srcslot->tts_values[natt]; dstslot->tts_isnull[natt] = srcslot->tts_isnull[natt]; } - dstslot->tts_nvalid = srcdesc->natts; + if(dstdesc->natts > srcdesc->natts) + { + for (int natt = srcdesc->natts; natt < dstdesc->natts; natt++) + { + dstslot->tts_values[natt] = (Datum) 0; + dstslot->tts_isnull[natt] = true; + } + } + + dstslot->tts_nvalid = dstdesc->natts; //srcdesc->natts; dstslot->tts_flags &= ~TTS_FLAG_EMPTY; /* make sure storage doesn't depend on external memory */ tts_virtual_materialize(dstslot); } +static HeapTuple +tts_virtual_copy_heap_tuple_natts(TupleTableSlot *slot, int natts) +{ + Assert(!TTS_EMPTY(slot)); + + return heap_form_tuple_ext(slot->tts_tupleDescriptor, + slot->tts_values, + slot->tts_isnull, + natts); +} + +static MinimalTuple +tts_virtual_copy_minimal_tuple_natts(TupleTableSlot *slot, int natts) +{ + Assert(!TTS_EMPTY(slot)); + + return heap_form_minimal_tuple_ext(slot->tts_tupleDescriptor, + slot->tts_values, + slot->tts_isnull, + 0, + natts); +} + static HeapTuple tts_virtual_copy_heap_tuple(TupleTableSlot *slot) { @@ -397,6 +435,12 @@ tts_heap_is_current_xact_tuple(TupleTableSlot *slot) static void tts_heap_materialize(TupleTableSlot *slot) +{ + tts_heap_materialize_ext(slot, -1); +} + +static void +tts_heap_materialize_ext(TupleTableSlot *slot, int natts) { HeapTupleTableSlot *hslot = (HeapTupleTableSlot *) slot; MemoryContext oldContext; @@ -417,9 +461,10 @@ tts_heap_materialize(TupleTableSlot *slot) hslot->off = 0; if (!hslot->tuple) - hslot->tuple = heap_form_tuple(slot->tts_tupleDescriptor, + hslot->tuple = heap_form_tuple_ext(slot->tts_tupleDescriptor, slot->tts_values, - slot->tts_isnull); + slot->tts_isnull, + natts); else { /* @@ -427,7 +472,7 @@ tts_heap_materialize(TupleTableSlot *slot) * context of the given slot (else it would have TTS_FLAG_SHOULDFREE * set). Copy the tuple into the given slot's memory context. */ - hslot->tuple = heap_copytuple(hslot->tuple); + hslot->tuple = heap_copytuple_ext(hslot->tuple, slot, natts); } slot->tts_flags |= TTS_FLAG_SHOULDFREE; @@ -483,6 +528,29 @@ tts_heap_copy_minimal_tuple(TupleTableSlot *slot, Size extra) return minimal_tuple_from_heap_tuple(hslot->tuple, extra); } +static HeapTuple +tts_heap_copy_heap_tuple_natts(TupleTableSlot *slot, int attnum) +{ + HeapTupleTableSlot *hslot = (HeapTupleTableSlot *) slot; + + Assert(!TTS_EMPTY(slot)); + if (!hslot->tuple) + tts_heap_materialize_ext(slot, attnum); + + return heap_copytuple_ext(hslot->tuple, slot, attnum); +} + +static MinimalTuple +tts_heap_copy_minimal_tuple_natts(TupleTableSlot *slot, int attnum) +{ + HeapTupleTableSlot *hslot = (HeapTupleTableSlot *) slot; + + if (!hslot->tuple) + tts_heap_materialize_ext(slot, attnum); + + return minimal_tuple_from_heap_tuple(hslot->tuple, 0); +} + static void tts_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple, bool shouldFree) { @@ -584,7 +652,7 @@ tts_minimal_is_current_xact_tuple(TupleTableSlot *slot) } static void -tts_minimal_materialize(TupleTableSlot *slot) +tts_minimal_materialize_ext(TupleTableSlot *slot, int natts) { MinimalTupleTableSlot *mslot = (MinimalTupleTableSlot *) slot; MemoryContext oldContext; @@ -606,10 +674,11 @@ tts_minimal_materialize(TupleTableSlot *slot) if (!mslot->mintuple) { - mslot->mintuple = heap_form_minimal_tuple(slot->tts_tupleDescriptor, + mslot->mintuple = heap_form_minimal_tuple_ext(slot->tts_tupleDescriptor, slot->tts_values, slot->tts_isnull, - 0); + 0, + natts); } else { @@ -632,16 +701,32 @@ tts_minimal_materialize(TupleTableSlot *slot) MemoryContextSwitchTo(oldContext); } +static void +tts_minimal_materialize(TupleTableSlot *slot) +{ + tts_minimal_materialize_ext(slot, -1); +} + static void tts_minimal_copyslot(TupleTableSlot *dstslot, TupleTableSlot *srcslot) { MemoryContext oldcontext; MinimalTuple mintuple; - oldcontext = MemoryContextSwitchTo(dstslot->tts_mcxt); - mintuple = ExecCopySlotMinimalTuple(srcslot); - MemoryContextSwitchTo(oldcontext); - + if(dstslot->tts_tupleDescriptor->natts != srcslot->tts_tupleDescriptor->natts) + { + int natts = (dstslot->tts_tupleDescriptor->natts < srcslot->tts_tupleDescriptor->natts ? dstslot->tts_tupleDescriptor->natts : srcslot->tts_tupleDescriptor->natts); + oldcontext = MemoryContextSwitchTo(dstslot->tts_mcxt); + mintuple = ExecCopySlotMinimalTupleNatts(srcslot, natts); + MemoryContextSwitchTo(oldcontext); + } + else + { + oldcontext = MemoryContextSwitchTo(dstslot->tts_mcxt); + mintuple = ExecCopySlotMinimalTuple(srcslot); + MemoryContextSwitchTo(oldcontext); + } + dstslot->tts_remoteOid = srcslot->tts_remoteOid; ExecStoreMinimalTuple(mintuple, dstslot, true); } @@ -678,6 +763,28 @@ tts_minimal_copy_minimal_tuple(TupleTableSlot *slot, Size extra) return heap_copy_minimal_tuple(mslot->mintuple, extra); } +static HeapTuple +tts_minimal_copy_heap_tuple_natts(TupleTableSlot *slot, int natts) +{ + MinimalTupleTableSlot *mslot = (MinimalTupleTableSlot *) slot; + + if (!mslot->mintuple) + tts_minimal_materialize_ext(slot, natts); + + return heap_tuple_from_minimal_tuple(mslot->mintuple); +} + +static MinimalTuple +tts_minimal_copy_minimal_tuple_natts(TupleTableSlot *slot, int natts) +{ + MinimalTupleTableSlot *mslot = (MinimalTupleTableSlot *) slot; + + if (!mslot->mintuple) + tts_minimal_materialize_ext(slot, natts); + + return heap_copy_minimal_tuple(mslot->mintuple, 0); +} + static void tts_minimal_store_tuple(TupleTableSlot *slot, MinimalTuple mtup, bool shouldFree) { @@ -801,7 +908,7 @@ tts_buffer_is_current_xact_tuple(TupleTableSlot *slot) } static void -tts_buffer_heap_materialize(TupleTableSlot *slot) +tts_buffer_heap_materialize_ext(TupleTableSlot *slot, int natts) { BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot; MemoryContext oldContext; @@ -830,13 +937,14 @@ tts_buffer_heap_materialize(TupleTableSlot *slot) * tuples in a buffer slot, which then also needs to be * materializable. */ - bslot->base.tuple = heap_form_tuple(slot->tts_tupleDescriptor, + bslot->base.tuple = heap_form_tuple_ext(slot->tts_tupleDescriptor, slot->tts_values, - slot->tts_isnull); + slot->tts_isnull, + natts); } else { - bslot->base.tuple = heap_copytuple(bslot->base.tuple); + bslot->base.tuple = heap_copytuple_ext(bslot->base.tuple, slot, natts); /* * A heap tuple stored in a BufferHeapTupleTableSlot should have a @@ -859,6 +967,12 @@ tts_buffer_heap_materialize(TupleTableSlot *slot) MemoryContextSwitchTo(oldContext); } +static void +tts_buffer_heap_materialize(TupleTableSlot *slot) +{ + tts_buffer_heap_materialize_ext(slot, -1); +} + static void tts_buffer_heap_copyslot(TupleTableSlot *dstslot, TupleTableSlot *srcslot) { @@ -874,12 +988,14 @@ tts_buffer_heap_copyslot(TupleTableSlot *dstslot, TupleTableSlot *srcslot) TTS_SHOULDFREE(srcslot) || !bsrcslot->base.tuple) { + int natts; MemoryContext oldContext; + natts = (dstslot->tts_tupleDescriptor->natts < srcslot->tts_tupleDescriptor->natts ? dstslot->tts_tupleDescriptor->natts : srcslot->tts_tupleDescriptor->natts); ExecClearTuple(dstslot); dstslot->tts_flags &= ~TTS_FLAG_EMPTY; oldContext = MemoryContextSwitchTo(dstslot->tts_mcxt); - bdstslot->base.tuple = ExecCopySlotHeapTuple(srcslot); + bdstslot->base.tuple = ExecCopySlotHeapTupleNatts(srcslot, natts); dstslot->tts_flags |= TTS_FLAG_SHOULDFREE; MemoryContextSwitchTo(oldContext); } @@ -914,6 +1030,19 @@ tts_buffer_heap_get_heap_tuple(TupleTableSlot *slot) return bslot->base.tuple; } +static HeapTuple +tts_buffer_heap_copy_heap_tuple_natts(TupleTableSlot *slot, int attnum) +{ + BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot; + + Assert(!TTS_EMPTY(slot)); + + if (!bslot->base.tuple) + tts_buffer_heap_materialize_ext(slot, attnum); + + return heap_copytuple_ext(bslot->base.tuple, slot, attnum); +} + static HeapTuple tts_buffer_heap_copy_heap_tuple(TupleTableSlot *slot) { @@ -927,6 +1056,19 @@ tts_buffer_heap_copy_heap_tuple(TupleTableSlot *slot) return heap_copytuple(bslot->base.tuple); } +static MinimalTuple +tts_buffer_copy_minimal_tuple_natts(TupleTableSlot *slot, int natts) +{ + BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot; + + Assert(!TTS_EMPTY(slot)); + + if (!bslot->base.tuple) + tts_buffer_heap_materialize_ext(slot, natts); + + return minimal_tuple_from_heap_tuple(bslot->base.tuple, 0); +} + static MinimalTuple tts_buffer_heap_copy_minimal_tuple(TupleTableSlot *slot, Size extra) { @@ -1274,7 +1416,9 @@ const TupleTableSlotOps TTSOpsVirtual = { .get_heap_tuple = NULL, .get_minimal_tuple = NULL, .copy_heap_tuple = tts_virtual_copy_heap_tuple, - .copy_minimal_tuple = tts_virtual_copy_minimal_tuple + .copy_minimal_tuple = tts_virtual_copy_minimal_tuple, + .copy_heap_tuple_ext = tts_virtual_copy_heap_tuple_natts, + .copy_minimal_tuple_ext = tts_virtual_copy_minimal_tuple_natts }; const TupleTableSlotOps TTSOpsHeapTuple = { @@ -1292,7 +1436,9 @@ const TupleTableSlotOps TTSOpsHeapTuple = { /* A heap tuple table slot can not "own" a minimal tuple. */ .get_minimal_tuple = NULL, .copy_heap_tuple = tts_heap_copy_heap_tuple, - .copy_minimal_tuple = tts_heap_copy_minimal_tuple + .copy_minimal_tuple = tts_heap_copy_minimal_tuple, + .copy_heap_tuple_ext = tts_heap_copy_heap_tuple_natts, + .copy_minimal_tuple_ext = tts_heap_copy_minimal_tuple_natts }; const TupleTableSlotOps TTSOpsMinimalTuple = { @@ -1310,7 +1456,9 @@ const TupleTableSlotOps TTSOpsMinimalTuple = { .get_heap_tuple = NULL, .get_minimal_tuple = tts_minimal_get_minimal_tuple, .copy_heap_tuple = tts_minimal_copy_heap_tuple, - .copy_minimal_tuple = tts_minimal_copy_minimal_tuple + .copy_minimal_tuple = tts_minimal_copy_minimal_tuple, + .copy_heap_tuple_ext = tts_minimal_copy_heap_tuple_natts, + .copy_minimal_tuple_ext = tts_minimal_copy_minimal_tuple_natts }; const TupleTableSlotOps TTSOpsBufferHeapTuple = { @@ -1328,7 +1476,9 @@ const TupleTableSlotOps TTSOpsBufferHeapTuple = { /* A buffer heap tuple table slot can not "own" a minimal tuple. */ .get_minimal_tuple = NULL, .copy_heap_tuple = tts_buffer_heap_copy_heap_tuple, - .copy_minimal_tuple = tts_buffer_heap_copy_minimal_tuple + .copy_minimal_tuple = tts_buffer_heap_copy_minimal_tuple, + .copy_heap_tuple_ext = tts_buffer_heap_copy_heap_tuple_natts, + .copy_minimal_tuple_ext = tts_buffer_copy_minimal_tuple_natts }; @@ -1867,8 +2017,7 @@ ExecStoreAllNullTuple(TupleTableSlot *slot) * Until the slot is materialized, the contents of the slot depend on the * datum. */ -void -ExecStoreHeapTupleDatum(Datum data, TupleTableSlot *slot) +void ExecStoreHeapTupleDatum(Datum data, TupleTableSlot *slot) { HeapTupleData tuple = {0}; HeapTupleHeader td; @@ -1878,6 +2027,7 @@ ExecStoreHeapTupleDatum(Datum data, TupleTableSlot *slot) tuple.t_len = HeapTupleHeaderGetDatumLength(td); tuple.t_self = td->t_ctid; tuple.t_data = td; + tuple.t_tableOid = InvalidOid; ExecClearTuple(slot); diff --git a/src/backend/optimizer/util/tlist.c b/src/backend/optimizer/util/tlist.c index 752ea9222f6..3c99c72d38c 100644 --- a/src/backend/optimizer/util/tlist.c +++ b/src/backend/optimizer/util/tlist.c @@ -329,18 +329,37 @@ apply_tlist_labeling(List *dest_tlist, List *src_tlist) ListCell *ld, *ls; - Assert(list_length(dest_tlist) == list_length(src_tlist)); - forboth(ld, dest_tlist, ls, src_tlist) + foreach(ld, dest_tlist) { + bool has_src = false; TargetEntry *dest_tle = (TargetEntry *) lfirst(ld); - TargetEntry *src_tle = (TargetEntry *) lfirst(ls); - - Assert(dest_tle->resno == src_tle->resno); - dest_tle->resname = src_tle->resname; - dest_tle->ressortgroupref = src_tle->ressortgroupref; - dest_tle->resorigtbl = src_tle->resorigtbl; - dest_tle->resorigcol = src_tle->resorigcol; - dest_tle->resjunk = src_tle->resjunk; + + foreach(ls, src_tlist) + { + TargetEntry *src_tle = (TargetEntry *) lfirst(ls); + + if(dest_tle->resno == src_tle->resno) + { + has_src = true; + dest_tle->resname = src_tle->resname; + dest_tle->ressortgroupref = src_tle->ressortgroupref; + dest_tle->resorigtbl = src_tle->resorigtbl; + dest_tle->resorigcol = src_tle->resorigcol; + dest_tle->resjunk = src_tle->resjunk; + break; + } + } + + if(!has_src) + { + TargetEntry *new_entry = palloc0(sizeof(TargetEntry)); + new_entry->resname = dest_tle->resname; + new_entry->ressortgroupref = dest_tle->ressortgroupref; + new_entry->resorigtbl = dest_tle->resorigtbl; + new_entry->resorigcol = dest_tle->resorigcol; + new_entry->resjunk = dest_tle->resjunk; + src_tlist = lappend(src_tlist, new_entry); + } } } diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h index 77a6c48fd71..338c123402f 100644 --- a/src/include/access/htup_details.h +++ b/src/include/access/htup_details.h @@ -794,10 +794,19 @@ HeapTupleClearHeapOnly(const HeapTupleData *tuple) /* prototypes for functions in common/heaptuple.c */ extern Size heap_compute_data_size(TupleDesc tupleDesc, const Datum *values, const bool *isnull); +extern Size heap_compute_data_size_ext(TupleDesc tupleDesc, + const Datum *values, + const bool *isnull, + int attnum); + extern void heap_fill_tuple(TupleDesc tupleDesc, const Datum *values, const bool *isnull, char *data, Size data_size, uint16 *infomask, uint8 *bit); +extern void heap_fill_tuple_ext(TupleDesc tupleDesc, + const Datum *values, const bool *isnull, + char *data, Size data_size, + uint16 *infomask, uint8 *bit, int natts); extern bool heap_attisnull(HeapTuple tup, int attnum, TupleDesc tupleDesc); extern Datum nocachegetattr(HeapTuple tup, int attnum, TupleDesc tupleDesc); @@ -806,10 +815,13 @@ extern Datum heap_getsysattr(HeapTuple tup, int attnum, TupleDesc tupleDesc, extern Datum getmissingattr(TupleDesc tupleDesc, int attnum, bool *isnull); extern HeapTuple heap_copytuple(HeapTuple tuple); +extern HeapTuple heap_copytuple_ext(HeapTuple tuple, void *slot, int dstnatts); extern void heap_copytuple_with_tuple(HeapTuple src, HeapTuple dest); extern Datum heap_copy_tuple_as_datum(HeapTuple tuple, TupleDesc tupleDesc); extern HeapTuple heap_form_tuple(TupleDesc tupleDescriptor, const Datum *values, const bool *isnull); +extern HeapTuple heap_form_tuple_ext(TupleDesc tupleDescriptor, + const Datum *values, const bool *isnull, int natts); extern HeapTuple heap_modify_tuple(HeapTuple tuple, TupleDesc tupleDesc, const Datum *replValues, @@ -827,6 +839,8 @@ extern void heap_freetuple(HeapTuple htup); extern MinimalTuple heap_form_minimal_tuple(TupleDesc tupleDescriptor, const Datum *values, const bool *isnull, Size extra); +extern MinimalTuple heap_form_minimal_tuple_ext(TupleDesc tupleDescriptor, + const Datum *values, const bool *isnull, Size extra, int natts); extern void heap_free_minimal_tuple(MinimalTuple mtup); extern MinimalTuple heap_copy_minimal_tuple(MinimalTuple mtup, Size extra); extern HeapTuple heap_tuple_from_minimal_tuple(MinimalTuple mtup); diff --git a/src/include/executor/tuptable.h b/src/include/executor/tuptable.h index d7e8e72aeae..75e0d69d46d 100644 --- a/src/include/executor/tuptable.h +++ b/src/include/executor/tuptable.h @@ -141,6 +141,7 @@ typedef struct TupleTableSlot MemoryContext tts_mcxt; /* slot itself is in this context */ ItemPointerData tts_tid; /* stored tuple's tid */ Oid tts_tableOid; /* table oid of tuple */ + Oid tts_remoteOid; } TupleTableSlot; /* routines for a TupleTableSlot implementation */ @@ -239,6 +240,26 @@ struct TupleTableSlotOps * with the minimal tuple without the need for an additional allocation. */ MinimalTuple (*copy_minimal_tuple) (TupleTableSlot *slot, Size extra); + + /* + * Return a copy of heap tuple's N atts representing the contents of the slot. + * The copy needs to be palloc'd in the current memory context. The slot + * itself is expected to remain unaffected. It is *not* expected to have + * meaningful "system columns" in the copy. The copy is not be "owned" by + * the slot i.e. the caller has to take responsibility to free memory + * consumed by the slot. + */ + HeapTuple (*copy_heap_tuple_ext) (TupleTableSlot *slot, int natts); + + /* + * Return a copy of minimal tuple's N atts representing the contents of the slot. + * The copy needs to be palloc'd in the current memory context. The slot + * itself is expected to remain unaffected. It is *not* expected to have + * meaningful "system columns" in the copy. The copy is not be "owned" by + * the slot i.e. the caller has to take responsibility to free memory + * consumed by the slot. + */ + MinimalTuple (*copy_minimal_tuple_ext) (TupleTableSlot *slot, int natts); }; /* @@ -500,6 +521,14 @@ ExecMaterializeSlot(TupleTableSlot *slot) * ExecCopySlotHeapTuple - return HeapTuple allocated in caller's context */ static inline HeapTuple +ExecCopySlotHeapTupleNatts(TupleTableSlot *slot, int natts) +{ + Assert(!TTS_EMPTY(slot)); + + return slot->tts_ops->copy_heap_tuple_ext(slot, natts); +} + + static inline HeapTuple ExecCopySlotHeapTuple(TupleTableSlot *slot) { Assert(!TTS_EMPTY(slot)); @@ -507,6 +536,15 @@ ExecCopySlotHeapTuple(TupleTableSlot *slot) return slot->tts_ops->copy_heap_tuple(slot); } +/* + * ExecCopySlotMinimalTuple - return MinimalTuple allocated in caller's context + */ +static inline MinimalTuple +ExecCopySlotMinimalTupleNatts(TupleTableSlot *slot, int natts) +{ + return slot->tts_ops->copy_minimal_tuple_ext(slot, natts); +} + /* * ExecCopySlotMinimalTuple - return MinimalTuple allocated in caller's context */ @@ -544,9 +582,18 @@ ExecCopySlot(TupleTableSlot *dstslot, TupleTableSlot *srcslot) { Assert(!TTS_EMPTY(srcslot)); Assert(srcslot != dstslot); + + /* This assert supposes that we always have same number + * of attributes and is not relevant in new case + */ +/* Assert(dstslot->tts_tupleDescriptor->natts == srcslot->tts_tupleDescriptor->natts); +*/ + /* If source slot has less attributes then target - copy source and + * set target to nulls. Otherwise copy only leading attributes and set + * target natts to source counter */ dstslot->tts_ops->copyslot(dstslot, srcslot); return dstslot; -- 2.43.0 [application/octet-stream] v1-0002-add-remote-tableoid-param.patch (16.1K, ../../CAN-LCVMz58ukZ7ubGXiLuTeFE7wWmSwDw4URpF0q1ejzRvqbzg@mail.gmail.com/5-v1-0002-add-remote-tableoid-param.patch) download | inline diff: From 32deb2248ea65f83ca5cdc81459648486d80eac4 Mon Sep 17 00:00:00 2001 From: Nikita Malakhov <[email protected]> Date: Tue, 21 Apr 2026 14:10:39 +0300 Subject: [PATCH] [POC] There is very old and well-known problem in FDW wrapper - while deleteing or updating rows of partitioned tables via FDW the FDW engine does not consider partitions which rows belong to, and when user tries to delete or update just 1 row - actually all rows with the same TID are deleted/updated across all partitions. There was previous approach to this problem [1] but it had some unsolved problems resulted in segfaults. Current solution appears to be more complex and requires core modification allowing copying tuples with different amount of attributes. This core patch adds new field in exec params to keep remote table OID and use it along with the TID for delete and update operations. [1] https://www.postgresql.org/message-id/flat/CAPmGK15CQK-oYFMAyq%2BrR0rQapUHtvAGuGgY5ahERHzZ4tmC8g%40mail.gmail.com#4321975bbd1af71c78d45d9a441e8458 --- src/backend/executor/execMain.c | 4 ++ src/backend/executor/nodeForeignscan.c | 2 + src/backend/optimizer/path/allpaths.c | 19 ++++++++ src/backend/optimizer/plan/createplan.c | 19 ++++++++ src/backend/optimizer/plan/initsplan.c | 43 ++++++++++++++++++ src/backend/optimizer/plan/planner.c | 14 +++--- src/backend/optimizer/plan/setrefs.c | 59 ++++++++++++++++++++++++- src/backend/optimizer/plan/subselect.c | 7 ++- src/backend/optimizer/util/appendinfo.c | 27 +++++++++++ src/backend/optimizer/util/relnode.c | 21 +++++++++ src/backend/utils/adt/ruleutils.c | 2 +- src/include/nodes/pathnodes.h | 7 +++ src/include/nodes/primnodes.h | 1 + 13 files changed, 217 insertions(+), 8 deletions(-) diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 4b30f768680..b7b3b73941e 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -2621,6 +2621,10 @@ ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist) resname); if (!AttributeNumberIsValid(aerm->ctidAttNo)) elog(ERROR, "could not find junk %s column", resname); + + snprintf(resname, sizeof(resname), "tableoid%u", erm->rowmarkId); + aerm->toidAttNo = ExecFindJunkAttributeInTlist(targetlist, + resname); } else { diff --git a/src/backend/executor/nodeForeignscan.c b/src/backend/executor/nodeForeignscan.c index 6f0daddce07..21b3e9a961b 100644 --- a/src/backend/executor/nodeForeignscan.c +++ b/src/backend/executor/nodeForeignscan.c @@ -65,6 +65,8 @@ ForeignNext(ForeignScanState *node) * Insert valid value into tableoid, the only actually-useful system * column. */ + + slot->tts_remoteOid = slot->tts_tableOid; if (plan->fsSystemCol && !TupIsNull(slot)) slot->tts_tableOid = RelationGetRelid(node->ss.ss_currentRelation); diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 61093f222a1..006ab5f89d6 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -1161,6 +1161,25 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel, (Node *) rel->reltarget->exprs, 1, &appinfo); + /* Do it if fdw is partition */ + if (planner_rt_fetch(childRTindex, root)->relkind == RELKIND_FOREIGN_TABLE && + !bms_is_empty(root->glob->foreignParamIDs)) + { + foreach(lc, root->processed_tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Param *param = (Param *) tle->expr; + + if (tle->resjunk && IsA(param, Param) && + IS_FOREIGN_PARAM(root, param) && + param->target_rte == childRTindex) // TODO same for another case + { + childrel->reltarget->exprs = + lappend(childrel->reltarget->exprs, param); + } + } + } + /* * We have to make child entries in the EquivalenceClass data * structures as well. This is needed either if the parent diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index de6a183da79..00ad2dcb07e 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -980,6 +980,25 @@ use_physical_tlist(PlannerInfo *root, Path *path, int flags) } } + /* + * Also, can't do it to a ForeignPath if the path is requested to emit + * Params generated by the FDW. + */ + if (IsA(path, ForeignPath) && + path->parent->relid == root->parse->resultRelation && + !bms_is_empty(root->glob->foreignParamIDs)) + { + foreach(lc, path->pathtarget->exprs) + { + Param *param = (Param *) lfirst(lc); + if (param && IsA(param, Param)) + { + Assert(IS_FOREIGN_PARAM(root, param)); + return false; + } + } + } + return true; } diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 96ee312ebdf..64be8a8b9d1 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -32,6 +32,7 @@ #include "optimizer/planner.h" #include "optimizer/restrictinfo.h" #include "parser/analyze.h" +#include "parser/parsetree.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -231,6 +232,38 @@ add_other_rels_to_query(PlannerInfo *root) * *****************************************************************************/ +/* + * add_params_to_result_rel + * If the query's final tlist contains Params the FDW generated, add + * targetlist entries for each such Param to the result relation. + */ +static void +add_params_to_result_rel(PlannerInfo *root, List *final_tlist) +{ + RelOptInfo *target_rel = find_base_rel(root, root->parse->resultRelation); + ListCell *lc; + + /* + * If no parameters have been generated by any FDWs, we certainly don't + * need to do anything here. + */ + if (bms_is_empty(root->glob->foreignParamIDs)) + return; + + foreach(lc, final_tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Param *param = (Param *) tle->expr; + + if (tle->resjunk && IsA(param, Param) && + IS_FOREIGN_PARAM(root, param) && + param->target_rte == target_rel->relid) + { + target_rel->reltarget->exprs = lappend(target_rel->reltarget->exprs, param); + } + } +} + /* * build_base_rel_tlists * Add targetlist entries for each var needed in the query's final tlist @@ -270,6 +303,16 @@ build_base_rel_tlists(PlannerInfo *root, List *final_tlist) list_free(having_vars); } } + + if (root->parse->commandType == CMD_UPDATE || + root->parse->commandType == CMD_DELETE) + { + int result_relation = root->parse->resultRelation; + + if (planner_rt_fetch(result_relation, root)->relkind == RELKIND_FOREIGN_TABLE) + add_params_to_result_rel(root, final_tlist); + + } } /* diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 4ec76ce31a9..d168368c2fe 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -375,6 +375,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->foreignParamIDs = NULL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -603,12 +604,15 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, } /* - * If any Params were generated, run through the plan tree and compute - * each plan node's extParam/allParam sets. Ideally we'd merge this into - * set_plan_references' tree traversal, but for now it has to be separate - * because we need to visit subplans before not after main plan. + * If any Params were generated by the planner not by FDWs, run through + * the plan tree and compute each plan node's extParam/allParam sets. + * (Params added by FDWs are irrelevant for parameter change signaling.) + * Ideally we'd merge this into set_plan_references' tree traversal, but + * for now it has to be separate because we need to visit subplans before + * not after main plan. */ - if (glob->paramExecTypes != NIL) + if (glob->paramExecTypes != NIL && + bms_num_members(glob->foreignParamIDs) < list_length(glob->paramExecTypes)) { Assert(list_length(glob->subplans) == list_length(glob->subroots)); forboth(lp, glob->subplans, lr, glob->subroots) diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index ff0e875f2a2..87e97360eae 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -1901,7 +1901,8 @@ set_append_references(PlannerInfo *root, { Plan *p = (Plan *) linitial(aplan->appendplans); - if (p->parallel_aware == aplan->plan.parallel_aware) + if ((p->parallel_aware == aplan->plan.parallel_aware) && + (list_length(((Plan *) aplan)->targetlist) == list_length(p->targetlist))) { Plan *result; @@ -3310,7 +3311,42 @@ fix_join_expr_mutator(Node *node, fix_join_expr_context *context) } /* Special cases (apply only AFTER failing to match to lower tlist) */ if (IsA(node, Param)) + { + Param *param = (Param *) node; + + /* + * If the Param is a PARAM_EXEC Param generated by an FDW, it should + * have bubbled up from a lower plan node; convert it into a simple + * Var referencing the output of the subplan. + * + * Note: set_join_references() would have kept has_non_vars=true for + * the subplan emitting the Param since it effectively belong to the + * result relation and that relation can never be the nullable side of + * an outer join. + */ + if (IS_FOREIGN_PARAM(context->root, param)) + { + if (context->outer_itlist && context->outer_itlist->has_non_vars) + { + newvar = search_indexed_tlist_for_non_var((Expr *) node, + context->outer_itlist, + OUTER_VAR); + if (newvar) + return (Node *) newvar; + } + if (context->inner_itlist && context->inner_itlist->has_non_vars) + { + newvar = search_indexed_tlist_for_non_var((Expr *) node, + context->inner_itlist, + INNER_VAR); + if (newvar) + return (Node *) newvar; + } + // XXX Is it an error to be here? + } + /* If not, do fix_param_node() */ return fix_param_node(context->root, (Param *) node); + } if (IsA(node, AlternativeSubPlan)) return fix_join_expr_mutator(fix_alternative_subplan(context->root, (AlternativeSubPlan *) node, @@ -3421,7 +3457,28 @@ fix_upper_expr_mutator(Node *node, fix_upper_expr_context *context) } /* Special cases (apply only AFTER failing to match to lower tlist) */ if (IsA(node, Param)) + { + Param *param = (Param *) node; + /* + * If the Param is a PARAM_EXEC Param generated by an FDW, it should + * have bubbled up from a lower plan node; convert it into a simple + * Var referencing the output of the subplan. + */ + if (IS_FOREIGN_PARAM(context->root, param)) + { + if (context->subplan_itlist->has_non_vars) + { + newvar = search_indexed_tlist_for_non_var((Expr *) node, + context->subplan_itlist, + context->newvarno); + if (newvar) + return (Node *) newvar; + } + // XXX Is it an error to be here? + } + /* If not, do fix_param_node() */ return fix_param_node(context->root, (Param *) node); + } if (IsA(node, Aggref)) { Aggref *aggref = (Aggref *) node; diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index ccec1eaa7fe..375fdf27e53 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -3192,7 +3192,12 @@ finalize_primnode(Node *node, finalize_primnode_context *context) { int paramid = ((Param *) node)->paramid; - context->paramids = bms_add_member(context->paramids, paramid); + /* + * Params added by FDWs are irrelevant for parameter change + * signaling. + */ + if (!bms_is_member(paramid, context->root->glob->foreignParamIDs)) + context->paramids = bms_add_member(context->paramids, paramid); } return false; /* no more to do here */ } diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c index 778e4662f6e..7089a267125 100644 --- a/src/backend/optimizer/util/appendinfo.c +++ b/src/backend/optimizer/util/appendinfo.c @@ -948,6 +948,29 @@ add_row_identity_var(PlannerInfo *root, Var *orig_var, root->processed_tlist = lappend(root->processed_tlist, tle); } +static void +fix_foreign_params(PlannerInfo *root, List *tlist) +{ + ListCell *lc; + + foreach(lc, tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Param *param = (Param *) tle->expr; + + if (tle->resjunk && IsA(param, Param) && + param->paramkind == PARAM_EXEC && + param->paramid == -1) + { + param->paramid = list_length(root->glob->paramExecTypes); + root->glob->paramExecTypes = + lappend_oid(root->glob->paramExecTypes, param->paramtype); + root->glob->foreignParamIDs = + bms_add_member(root->glob->foreignParamIDs, param->paramid); + } + } +} + /* * add_row_identity_columns * @@ -992,8 +1015,12 @@ add_row_identity_columns(PlannerInfo *root, Index rtindex, fdwroutine = GetFdwRoutineForRelation(target_relation, false); if (fdwroutine->AddForeignUpdateTargets != NULL) + { + fdwroutine->AddForeignUpdateTargets(root, rtindex, target_rte, target_relation); + fix_foreign_params(root, root->processed_tlist); + } /* * For UPDATE, we need to make the FDW fetch unchanged columns by diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 3fc2c2f71d0..655b2b00bd7 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -1320,6 +1320,27 @@ build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel, } continue; } + /* + * We allow FDWs to have PARAM_EXEC Params here. + */ + else if (IsA(var, Param)) + { + Param *param = (Param *) var; + + Assert(IS_FOREIGN_PARAM(root, param)); + + joinrel->reltarget->exprs = + lappend(joinrel->reltarget->exprs, param); + + /* + * Estimate using the type info (Note: keep this in sync with + * set_rel_width()) + */ + joinrel->reltarget->width += + get_typavgwidth(param->paramtype, param->paramtypmod); + + continue; + } /* * Otherwise, anything in a baserel or joinrel targetlist ought to be diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 090e8cc28c1..d2357018d0a 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -9374,7 +9374,7 @@ get_parameter(Param *param, deparse_context *context) * It's a bug if we get here for anything except PARAM_EXTERN Params, but * in production builds printing $N seems more useful than failing. */ - Assert(param->paramkind == PARAM_EXTERN); + Assert(param->paramkind == PARAM_EXTERN || param->paramkind == PARAM_EXEC); appendStringInfo(context->buf, "$%d", param->paramid); } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 693b879f76d..3ad051d0af4 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -265,6 +265,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + /* PARAM_EXEC Params generated by FDWs */ + Bitmapset *foreignParamIDs; + /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); @@ -277,6 +280,10 @@ typedef struct PlannerGlobal #define planner_subplan_get_plan(root, subplan) \ ((Plan *) list_nth((root)->glob->subplans, (subplan)->plan_id - 1)) +/* macro for checking if a Param is a PARAM_EXEC Param generated by an FDW */ +#define IS_FOREIGN_PARAM(root, param) \ + ((param)->paramkind == PARAM_EXEC && \ + bms_is_member((param)->paramid, (root)->glob->foreignParamIDs)) /*---------- * PlannerInfo diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfc946c20b..b85c54e8ec2 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -402,6 +402,7 @@ typedef struct Param Oid paramcollid; /* token location, or -1 if unknown */ ParseLoc location; + Index target_rte; } Param; /* -- 2.43.0 ^ permalink raw reply [nested|flat] 16+ messages in thread
end of thread, other threads:[~2026-04-21 11:34 UTC | newest] Thread overview: 16+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2018-10-16 04:04 [PATCH 3/5] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2018-10-16 04:04 [PATCH 2/2] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2018-10-16 04:04 [PATCH 1/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2018-10-16 04:04 [PATCH 2/3] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2018-10-16 04:04 [PATCH 2/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2018-10-16 04:04 [PATCH 1/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2018-10-16 04:04 [PATCH 2/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2018-10-16 04:04 [PATCH 1/4] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2019-03-01 04:32 [PATCH] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2019-03-01 04:32 [PATCH] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2019-03-01 04:32 [PATCH 2/2] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2019-03-01 04:32 [PATCH] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2019-03-01 04:32 [PATCH 2/6] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2019-03-01 04:32 [PATCH 1/2] Remove entries that haven't been used for a certain time Kyotaro Horiguchi <[email protected]> 2019-12-20 01:09 [PATCH v37 03/11] Allow to prolong life span of transition tables until transaction end Yugo Nagata <[email protected]> 2026-04-21 11:34 Re: [(known) BUG] DELETE/UPDATE more than one row in partitioned foreign table Nikita Malakhov <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox