{"Entry":{"collection":"plan","key":"memoize","name":"Memoize","aliases":["Memoize","T_Memoize"],"metadata":{"aliases":["Memoize","T_Memoize"],"category":"Materialization","content_hash":"f89fcc4f6ea3f2e1708296ae392fd1c44fe7caa8a6c932d46ae40983f766b96b","imported_at":"2026-09-30T00:40:44.158534+08:00","name":"Memoize","name_zh":"Memoize","slug":"memoize","summary":"Caches results from a parameterized child and reuses them when the same parameter values recur."}},"Definition":{"Collection":"plan","Key":"memoize","SourceDatabase":"center","Version":"18","SourceTable":"plan_node","SourceKey":"memoize","SourceRevision":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","Facts":{"comparison_data":{"initializer":"ExecInitMemoize","memory_mechanism":"eviction","node_tag":"T_Memoize","parallel_callbacks":["ExecMemoizeEstimate","ExecMemoizeInitializeDSM","ExecMemoizeInitializeWorker","ExecMemoizeRetrieveInstrumentation"],"partial_modes":[],"strategies":[],"text_names":["Memoize"]},"comparison_hash":"ce79083064802c6772e741ce60e19e9c932b05d1f1a5ed00e8f8f2e86d678f5b","description":["Caches results from a parameterized child and reuses them when the same parameter values recur."],"evidence_kind":"source and documentation","explain_names":["Memoize"],"explain_prefixes":["Parallel","Async"],"facts":[{"label":"Core node tag","value":"T_Memoize"},{"label":"Structured EXPLAIN Node Type","value":"Memoize"},{"label":"Inputs","value":"One parameterized child plan"},{"label":"Output","value":"Cached or newly produced child tuples"},{"label":"Executor initializer","value":"ExecInitMemoize"},{"label":"Memory mechanism","value":"eviction"}],"memory":{"description":"The Memoize cache evicts least-recently-used entries instead of spilling cached tuples to disk. If a result cannot fit, caching can enter bypass mode for that scan.","evidence":[{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/backend/executor/nodeMemoize.c","path":"src/backend/executor/nodeMemoize.c","sha256":"1dec8adecc70763c97957ed94510c4734a399223fd2f81f5534a30351c06c5d6","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"}],"mechanism":"eviction","source_notes":["The method of cache we use is a hash table. When the cache fills, we never spill tuples to disk, instead, we choose to evict the least recently used cache entry from the cache. We remember the least recently used entry by always pushing new entries and entries we look for onto the tail of a doubly linked list. This means that older items always bubble to the top of this LRU list.","It's possible when we're filling the cache for a given set of parameters that we're unable to free enough memory to store any more tuples. If this happens then we'll have already evicted all other cache entries. When caching another tuple would cause us to exceed our memory budget, we must free the entry that we're currently populating and move the state machine into MEMO_CACHE_BYPASS_MODE. This means that we'll not attempt to cache any further tuples for this particular scan. We don't have the memory for it. The state machine will be reset again on the next rescan. If the memory requirements to cache the next parameter's tuples are less demanding, then that may allow us to start putting useful entries back into the cache again.","cache_lookup Perform a lookup to see if we've already cached tuples based on the scan's current parameters. If we find an existing entry we move it to the end of the LRU list, set *found to true then return it. If we don't find an entry then we create a new one and add it to the end of the LRU list. We also update cache memory accounting and remove older entries if we go over the memory budget. If we managed to free enough memory we return the new entry, else we return NULL.","If we've gone over our memory budget, then we'll free up some space in the cache."]},"node_tag":"T_Memoize","parallel_callbacks":["ExecMemoizeEstimate","ExecMemoizeInitializeDSM","ExecMemoizeInitializeWorker","ExecMemoizeRetrieveInstrumentation"],"partial_modes":[],"related":[{"label":"EXPLAIN","url":"/wiki/sql/explain/?v=18"},{"label":"Using EXPLAIN","url":"/docs/18/using-explain.html"},{"label":"Parallel plans","url":"/docs/18/parallel-plans.html"},{"label":"enable_memoize","url":"/wiki/guc/enable_memoize/?v=18"},{"label":"hash_mem_multiplier","url":"/wiki/guc/hash_mem_multiplier/?v=18"}],"release":{"channel":"stable","label":"18.6","major":"18","ref":"PostgreSQL 18.6 source archive","revision":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","source_snapshot_utc":"","source_url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"},"runtime_verified":false,"sections":[{"paragraphs":["Structured formats use the Node Type above. Text-format spellings can also include operation, strategy, join type, scan direction or aggregation-stage attributes.","Text names recorded by this source: Memoize.","Parallel-aware and parallel-safe are different plan properties. A node running inside a parallel worker is not necessarily a parallel-aware node."],"title":"EXPLAIN names and attributes"},{"paragraphs":["The Memoize cache evicts least-recently-used entries instead of spilling cached tuples to disk. If a result cannot fit, caching can enter bypass mode for that scan.","The method of cache we use is a hash table. When the cache fills, we never spill tuples to disk, instead, we choose to evict the least recently used cache entry from the cache. We remember the least recently used entry by always pushing new entries and entries we look for onto the tail of a doubly linked list. This means that older items always bubble to the top of this LRU list.","It's possible when we're filling the cache for a given set of parameters that we're unable to free enough memory to store any more tuples. If this happens then we'll have already evicted all other cache entries. When caching another tuple would cause us to exceed our memory budget, we must free the entry that we're currently populating and move the state machine into MEMO_CACHE_BYPASS_MODE. This means that we'll not attempt to cache any further tuples for this particular scan. We don't have the memory for it. The state machine will be reset again on the next rescan. If the memory requirements to cache the next parameter's tuples are less demanding, then that may allow us to start putting useful entries back into the cache again.","cache_lookup Perform a lookup to see if we've already cached tuples based on the scan's current parameters. If we find an existing entry we move it to the end of the LRU list, set *found to true then return it. If we don't find an entry then we create a new one and add it to the end of the LRU list. We also update cache memory accounting and remove older entries if we go over the memory budget. If we managed to free enough memory we return the new entry, else we return NULL.","If we've gone over our memory budget, then we'll free up some space in the cache."],"title":"Memory and temporary storage"},{"paragraphs":["The source callbacks below can coordinate execution or collect worker instrumentation. Their presence is not a blanket claim that this node supports a shared parallel scan or shared state.","Callbacks in this build: ExecMemoizeEstimate, ExecMemoizeInitializeDSM, ExecMemoizeInitializeWorker, ExecMemoizeRetrieveInstrumentation."],"title":"Parallel execution and instrumentation"},{"paragraphs":["nodeMemoize.c Routines to handle caching of results from parameterized nodes","Memoize nodes are intended to sit above parameterized nodes in the plan tree in order to cache results from them. The intention here is that a repeat scan with a parameter value that has already been seen by the node can fetch tuples from the cache rather than having to re-scan the inner node all over again. The query planner may choose to make use of one of these when it thinks rescans for previously seen values are likely enough to warrant adding the additional node.","The method of cache we use is a hash table. When the cache fills, we never spill tuples to disk, instead, we choose to evict the least recently used cache entry from the cache. We remember the least recently used entry by always pushing new entries and entries we look for onto the tail of a doubly linked list. This means that older items always bubble to the top of this LRU list.","Sometimes our callers won't run their scans to completion. For example a semi-join only needs to run until it finds a matching tuple, and once it does, the join operator skips to the next outer tuple and does not execute the inner side again on that scan. Because of this, we must keep track of when a cache entry is complete, and by default, we know it is when we run out of tuples to read during the scan. However, there are cases where we can mark the cache entry as complete without exhausting the scan of all tuples. One case is unique joins, where the join operator knows that there will only be at most one match for any given outer tuple. In order to support such cases we allow the \"singlerow\" option to be set for the cache. This option marks the cache entry as complete after we read the first tuple from the subnode.","It's possible when we're filling the cache for a given set of parameters that we're unable to free enough memory to store any more tuples. If this happens then we'll have already evicted all other cache entries. When caching another tuple would cause us to exceed our memory budget, we must free the entry that we're currently populating and move the state machine into MEMO_CACHE_BYPASS_MODE. This means that we'll not attempt to cache any further tuples for this particular scan. We don't have the memory for it. The state machine will be reset again on the next rescan. If the memory requirements to cache the next parameter's tuples are less demanding, then that may allow us to start putting useful entries back into the cache again."],"title":"Executor implementation notes"},{"code":"case T_Memoize:\n\t\t\tpname = sname = \"Memoize\";\n\t\t\tbreak;","title":"EXPLAIN identity in core source"}],"source_inventory":{"executor":"src/backend/executor/execProcnode.c","explain":"src/backend/commands/explain.c","implementation":"src/backend/executor/nodeMemoize.c"},"sources":[{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/backend/commands/explain.c:1519","line":1519,"path":"src/backend/commands/explain.c","sha256":"34c86d6070224a0e981efef51f79101d6d505e5874f1684ace183034bab14bb4","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"},{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/backend/executor/execProcnode.c:330","line":330,"path":"src/backend/executor/execProcnode.c","sha256":"f8a06a3f539077249b20664b2812433db6d7bd12b2c0ca633525db43d06f112a","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"},{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/backend/executor/nodeMemoize.c","path":"src/backend/executor/nodeMemoize.c","sha256":"1dec8adecc70763c97957ed94510c4734a399223fd2f81f5534a30351c06c5d6","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"},{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/include/nodes/plannodes.h","path":"src/include/nodes/plannodes.h","sha256":"52422b327a8049fbbb20d8b96008a0fc0a6fafa60f7eff3c695d5b2e83830120","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"}],"strategies":[],"tables":[{"columns":[{"key":"label","label":"Text-format label"},{"key":"identity","label":"Structured node identity"}],"key":"explain-labels","rows":[{"identity":"Memoize","label":"Memoize"}],"title":"EXPLAIN labels in this source build"}]},"ManualEvidence":{"release":{"channel":"stable","label":"18.6","major":"18","ref":"PostgreSQL 18.6 source archive","revision":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","source_snapshot_utc":"","source_url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"},"sources":[{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/backend/commands/explain.c:1519","line":1519,"path":"src/backend/commands/explain.c","sha256":"34c86d6070224a0e981efef51f79101d6d505e5874f1684ace183034bab14bb4","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"},{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/backend/executor/execProcnode.c:330","line":330,"path":"src/backend/executor/execProcnode.c","sha256":"f8a06a3f539077249b20664b2812433db6d7bd12b2c0ca633525db43d06f112a","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"},{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/backend/executor/nodeMemoize.c","path":"src/backend/executor/nodeMemoize.c","sha256":"1dec8adecc70763c97957ed94510c4734a399223fd2f81f5534a30351c06c5d6","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"},{"archive_sha256":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","label":"src/include/nodes/plannodes.h","path":"src/include/nodes/plannodes.h","sha256":"52422b327a8049fbbb20d8b96008a0fc0a6fafa60f7eff3c695d5b2e83830120","url":"https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2"}]},"MeasuredEvidence":{"runtime_verified":false}},"Text":{"Collection":"plan","Key":"memoize","SourceDatabase":"center","Version":"18","Locale":"en","Title":"Memoize","Summary":"Caches results from a parameterized child and reuses them when the same parameter values recur.","BodyHTML":"\u003cp\u003eCaches results from a parameterized child and reuses them when the same parameter values recur.\u003c/p\u003e","SourceRevision":"555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f","ContentHash":"2c972bb734ba865d45d817ff8fac1c36b943538dadc1d0d2ef51ce296e1213ca","Payload":{"description":["Caches results from a parameterized child and reuses them when the same parameter values recur."],"related":[{"label":"EXPLAIN","url":"/wiki/sql/explain/?v=18"},{"label":"Using EXPLAIN","url":"/docs/18/using-explain.html"},{"label":"Parallel plans","url":"/docs/18/parallel-plans.html"},{"label":"enable_memoize","url":"/wiki/guc/enable_memoize/?v=18"},{"label":"hash_mem_multiplier","url":"/wiki/guc/hash_mem_multiplier/?v=18"}],"sections":[{"paragraphs":["Structured formats use the Node Type above. Text-format spellings can also include operation, strategy, join type, scan direction or aggregation-stage attributes.","Text names recorded by this source: Memoize.","Parallel-aware and parallel-safe are different plan properties. A node running inside a parallel worker is not necessarily a parallel-aware node."],"title":"EXPLAIN names and attributes"},{"paragraphs":["The Memoize cache evicts least-recently-used entries instead of spilling cached tuples to disk. If a result cannot fit, caching can enter bypass mode for that scan.","The method of cache we use is a hash table. When the cache fills, we never spill tuples to disk, instead, we choose to evict the least recently used cache entry from the cache. We remember the least recently used entry by always pushing new entries and entries we look for onto the tail of a doubly linked list. This means that older items always bubble to the top of this LRU list.","It's possible when we're filling the cache for a given set of parameters that we're unable to free enough memory to store any more tuples. If this happens then we'll have already evicted all other cache entries. When caching another tuple would cause us to exceed our memory budget, we must free the entry that we're currently populating and move the state machine into MEMO_CACHE_BYPASS_MODE. This means that we'll not attempt to cache any further tuples for this particular scan. We don't have the memory for it. The state machine will be reset again on the next rescan. If the memory requirements to cache the next parameter's tuples are less demanding, then that may allow us to start putting useful entries back into the cache again.","cache_lookup Perform a lookup to see if we've already cached tuples based on the scan's current parameters. If we find an existing entry we move it to the end of the LRU list, set *found to true then return it. If we don't find an entry then we create a new one and add it to the end of the LRU list. We also update cache memory accounting and remove older entries if we go over the memory budget. If we managed to free enough memory we return the new entry, else we return NULL.","If we've gone over our memory budget, then we'll free up some space in the cache."],"title":"Memory and temporary storage"},{"paragraphs":["The source callbacks below can coordinate execution or collect worker instrumentation. Their presence is not a blanket claim that this node supports a shared parallel scan or shared state.","Callbacks in this build: ExecMemoizeEstimate, ExecMemoizeInitializeDSM, ExecMemoizeInitializeWorker, ExecMemoizeRetrieveInstrumentation."],"title":"Parallel execution and instrumentation"},{"paragraphs":["nodeMemoize.c Routines to handle caching of results from parameterized nodes","Memoize nodes are intended to sit above parameterized nodes in the plan tree in order to cache results from them. The intention here is that a repeat scan with a parameter value that has already been seen by the node can fetch tuples from the cache rather than having to re-scan the inner node all over again. The query planner may choose to make use of one of these when it thinks rescans for previously seen values are likely enough to warrant adding the additional node.","The method of cache we use is a hash table. When the cache fills, we never spill tuples to disk, instead, we choose to evict the least recently used cache entry from the cache. We remember the least recently used entry by always pushing new entries and entries we look for onto the tail of a doubly linked list. This means that older items always bubble to the top of this LRU list.","Sometimes our callers won't run their scans to completion. For example a semi-join only needs to run until it finds a matching tuple, and once it does, the join operator skips to the next outer tuple and does not execute the inner side again on that scan. Because of this, we must keep track of when a cache entry is complete, and by default, we know it is when we run out of tuples to read during the scan. However, there are cases where we can mark the cache entry as complete without exhausting the scan of all tuples. One case is unique joins, where the join operator knows that there will only be at most one match for any given outer tuple. In order to support such cases we allow the \"singlerow\" option to be set for the cache. This option marks the cache entry as complete after we read the first tuple from the subnode.","It's possible when we're filling the cache for a given set of parameters that we're unable to free enough memory to store any more tuples. If this happens then we'll have already evicted all other cache entries. When caching another tuple would cause us to exceed our memory budget, we must free the entry that we're currently populating and move the state machine into MEMO_CACHE_BYPASS_MODE. This means that we'll not attempt to cache any further tuples for this particular scan. We don't have the memory for it. The state machine will be reset again on the next rescan. If the memory requirements to cache the next parameter's tuples are less demanding, then that may allow us to start putting useful entries back into the cache again."],"title":"Executor implementation notes"},{"code":"case T_Memoize:\n\t\t\tpname = sname = \"Memoize\";\n\t\t\tbreak;","title":"EXPLAIN identity in core source"}],"tables":[{"columns":[{"key":"label","label":"Text-format label"},{"key":"identity","label":"Structured node identity"}],"key":"explain-labels","rows":[{"identity":"Memoize","label":"Memoize"}],"title":"EXPLAIN labels in this source build"}]}},"RequestedLocale":"zh-Hans","Fallback":true,"Versions":["14","15","16","17","18","19","20"],"Locales":["en"],"Signatures":null,"Spellings":null,"SQLState":null,"Evidence":null}
