Thread

Commits

  1. doc: clarify MERGE PARTITIONS adjacency requirement

  1. Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-06-21T19:08:31Z

    I have been thinking about global temporary tables (that is, temporary
    tables whose definition is permanent, and visible to all sessions, but
    whose data is temporary, and local to each session), and I have a
    rough patch set implementing this.
    
    I didn't look closely at the previous patch attempting this, because
    it is quite old, and as others noted, it had significant design
    issues. So I have attempted to come up with a new design, which I have
    split into a series of patches, each focusing on a different aspect of
    the problem, which hopefully makes it easier to think about.
    
    
    0001 is the basic patch allowing the syntax CREATE GLOBAL TEMP TABLE
    to create a global temporary table with a new relpersistence of
    RELPERSISTENCE_GLOBAL_TEMP. Such tables are only allowed in
    non-temporary schemas.
    
    The majority of the code in this patch is establishing the basic
    infrastructure needed to manage these tables -- the first time a
    global temporary table is used in a session, it is initialised, which
    includes creating local storage for it, using local buffers, just like
    a local temporary table. All global temporary tables in use, and all
    storage created for them, is tracked through operations like
    (sub)transaction rollback, TRUNCATE, etc. and any storage remaining at
    backend exit is deleted.
    
    In the event of a backend crash, there is pre-existing code in
    RemovePgTempFiles() that will delete any temporary files left behind,
    if remove_temp_files_after_crash is on, but if that doesn't happen,
    the new initialisation code will automatically delete any existing
    storage for a relation before creating new storage.
    
    A shared hash table is used to track global temporary tables in use
    across all backends. This is used to prevent operations like ALTER
    TABLE from altering a table that is being used by some other backend,
    if the change would require a rewrite or scan of the table's contents,
    which isn't possible because one backend cannot access the local
    buffers of another.
    
    There's a large header comment in global_temp.c that explains the
    design in more detail.
    
    
    0002 adds support for indexes. The first time an index on a global
    temporary table is opened in a session other than the session that
    created it, an empty index is built in local storage using
    ambuildempty() (which the patch modifies to accept a fork number
    argument). If the table is not empty (the session had already added
    some data to the table before another session defined the index), then
    the index is marked invalid (more on that in 0009), and cannot be used
    in that session without doing a REINDEX.
    
    
    0003 adds support for sequences.
    
    
    0004 allows system catalog tables to be global temporary tables, and
    defines the first such example: pg_temp_class. The idea is that
    pg_temp_class has a subset of the columns of pg_class, allowing those
    properties to override the values from pg_class, allowing global
    temporary tables to operate independently in each session.
    
    In this commit, the only columns are oid, relfilenode, and
    reltablespace, allowing each session to independently track the
    location of the storage of global temporary tables. If a session
    executes CLUSTER, REINDEX, REPACK, TRUNCATE, or VACUUM FULL on a
    global temporary table, the updates are saved to pg_temp_class instead
    of pg_class, so they only affect that session.
    
    ALTER TABLE ... SET TABLESPACE works a little differently in that it
    updates reltablespace in both pg_temp_class and pg_class. This way,
    the change applies to the current session and any future sessions that
    use the table, but not to any other existing sessions that are already
    using it, which continue to use their own pg_temp_class.reltablespace
    values.
    
    There's another large header comment in pg_temp_class.h, explaining
    the design in more detail.
    
    (BTW, I intentionally chose the name pg_temp_class, rather than
    something like pg_global_temp_class, because I think perhaps this, and
    other similar catalog tables might possibly be used in the future for
    local temporary tables too, though I have not explored that idea in
    any detail.)
    
    
    0005 adds relation statistics columns to pg_temp_class (relpages,
    reltuples, relallvisible, and relallfrozen), and adjusts ANALYZE,
    CREATE INDEX, REPACK, VACUUM, and pg_clear/restore_relation_stats() to
    update pg_temp_class instead of pg_class, for global temporary tables,
    so each session gets its own relation-level statistics.
    
    
    0006 adds the VACUUM-related fields relfrozenxid and relminmxid to
    pg_temp_class. This is not quite so straightforward though. In
    addition to those fields, I added new fields tempfrozenxid and
    tempminmxid to the PGPROC structure. These are set by each session to
    the minimum values of relfrozenxid and relminmxid over all global
    temporary tables in use by that session. Then, when VACUUM is run, it
    sets pg_temp_class.relfrozenxid/relminmxid, based on the local
    contents of the table, and pg_class.relfrozenxid/relminmxid taking
    into account the contents of other sessions using global temporary
    tables. It's a little crude, because it can't see other
    relfrozenxid/relminmxid values on a per-table level for other
    sessions, but this is sufficient to allow
    pg_database.datfrozenxid/datminmxid to be advanced, provided that each
    session runs VACUUM from time to time.
    
    This still suffers from the same problem as local temporary tables
    though -- if a session uses a local or global temporary table, and
    then just sits there, without ever running VACUUM, there is no way to
    advance the pg_database fields, and eventually there will be a XID
    wraparound danger. Autovacuum doesn't help, because it can't vacuum
    temporary tables. It's not at all clear what can be done about that.
    It might be of some help to add a diagnostic function to identify the
    offending backend, though I have not done so here.
    
    
    0007 adds another global temporary system catalog table:
    pg_temp_statistic. This has the exact same set of columns as
    pg_statistic, but it is used to hold statistics about global temporary
    tables (and again, it could in theory also be used for local temporary
    tables).
    
    ANALYZE writes to pg_temp_statistic instead of pg_statistic for global
    temporary tables, and various selectivity estimating functions are
    updated to read from it, so each session gets its own local set of
    per-column statistics for the global temporary tables that it uses.
    
    The pg_stats view is updated to a UNION ALL query selecting from
    pg_statistic and pg_temp_statistic, so users can view their own
    statistics data in the usual way.
    
    
    0008 adds pg_temp_statistic_ext_data, which is exactly the same as
    pg_statistic_ext_data, except that it is a global temporary table used
    to store extended statistics data for global temporary tables. The
    views pg_stats_ext and pg_stats_ext_exprs are updated to include this.
    
    
    0009 adds a final global temporary system catalog table:
    pg_temp_index. As noted in 0002, a session needs to be able to mark an
    index on a global temporary table as invalid locally, if it was added
    by another session after this session had already populated the table.
    So pg_temp_index has indexrelid and indisvalid columns, so that the
    valid state of an index can be overridden locally.
    
    
    So far, I've focused on getting a set of patches that work, and these
    do seem to operate as expected. However, it seems quite likely that
    there are things that I have overlooked.
    
    I'm also aware that I haven't written any documentation yet, and I
    need to add more tests, but as a rough set of patches, I hope that
    they're in good enough shape for review.
    
    Regards,
    Dean
    
  2. Re: Global temporary tables

    Andrew Dunstan <andrew@dunslane.net> — 2026-06-21T22:05:55Z

    On 2026-06-21 Su 3:08 PM, Dean Rasheed wrote:
    > I have been thinking about global temporary tables (that is, temporary
    > tables whose definition is permanent, and visible to all sessions, but
    > whose data is temporary, and local to each session), and I have a
    > rough patch set implementing this.
    >
    > I didn't look closely at the previous patch attempting this, because
    > it is quite old, and as others noted, it had significant design
    > issues. So I have attempted to come up with a new design, which I have
    > split into a series of patches, each focusing on a different aspect of
    > the problem, which hopefully makes it easier to think about.
    >
    >
    > 0001 is the basic patch allowing the syntax CREATE GLOBAL TEMP TABLE
    > to create a global temporary table with a new relpersistence of
    > RELPERSISTENCE_GLOBAL_TEMP. Such tables are only allowed in
    > non-temporary schemas.
    >
    > The majority of the code in this patch is establishing the basic
    > infrastructure needed to manage these tables -- the first time a
    > global temporary table is used in a session, it is initialised, which
    > includes creating local storage for it, using local buffers, just like
    > a local temporary table. All global temporary tables in use, and all
    > storage created for them, is tracked through operations like
    > (sub)transaction rollback, TRUNCATE, etc. and any storage remaining at
    > backend exit is deleted.
    >
    > In the event of a backend crash, there is pre-existing code in
    > RemovePgTempFiles() that will delete any temporary files left behind,
    > if remove_temp_files_after_crash is on, but if that doesn't happen,
    > the new initialisation code will automatically delete any existing
    > storage for a relation before creating new storage.
    >
    > A shared hash table is used to track global temporary tables in use
    > across all backends. This is used to prevent operations like ALTER
    > TABLE from altering a table that is being used by some other backend,
    > if the change would require a rewrite or scan of the table's contents,
    > which isn't possible because one backend cannot access the local
    > buffers of another.
    >
    > There's a large header comment in global_temp.c that explains the
    > design in more detail.
    >
    >
    > 0002 adds support for indexes. The first time an index on a global
    > temporary table is opened in a session other than the session that
    > created it, an empty index is built in local storage using
    > ambuildempty() (which the patch modifies to accept a fork number
    > argument). If the table is not empty (the session had already added
    > some data to the table before another session defined the index), then
    > the index is marked invalid (more on that in 0009), and cannot be used
    > in that session without doing a REINDEX.
    >
    >
    > 0003 adds support for sequences.
    >
    >
    > 0004 allows system catalog tables to be global temporary tables, and
    > defines the first such example: pg_temp_class. The idea is that
    > pg_temp_class has a subset of the columns of pg_class, allowing those
    > properties to override the values from pg_class, allowing global
    > temporary tables to operate independently in each session.
    >
    > In this commit, the only columns are oid, relfilenode, and
    > reltablespace, allowing each session to independently track the
    > location of the storage of global temporary tables. If a session
    > executes CLUSTER, REINDEX, REPACK, TRUNCATE, or VACUUM FULL on a
    > global temporary table, the updates are saved to pg_temp_class instead
    > of pg_class, so they only affect that session.
    >
    > ALTER TABLE ... SET TABLESPACE works a little differently in that it
    > updates reltablespace in both pg_temp_class and pg_class. This way,
    > the change applies to the current session and any future sessions that
    > use the table, but not to any other existing sessions that are already
    > using it, which continue to use their own pg_temp_class.reltablespace
    > values.
    >
    > There's another large header comment in pg_temp_class.h, explaining
    > the design in more detail.
    >
    > (BTW, I intentionally chose the name pg_temp_class, rather than
    > something like pg_global_temp_class, because I think perhaps this, and
    > other similar catalog tables might possibly be used in the future for
    > local temporary tables too, though I have not explored that idea in
    > any detail.)
    >
    >
    > 0005 adds relation statistics columns to pg_temp_class (relpages,
    > reltuples, relallvisible, and relallfrozen), and adjusts ANALYZE,
    > CREATE INDEX, REPACK, VACUUM, and pg_clear/restore_relation_stats() to
    > update pg_temp_class instead of pg_class, for global temporary tables,
    > so each session gets its own relation-level statistics.
    >
    >
    > 0006 adds the VACUUM-related fields relfrozenxid and relminmxid to
    > pg_temp_class. This is not quite so straightforward though. In
    > addition to those fields, I added new fields tempfrozenxid and
    > tempminmxid to the PGPROC structure. These are set by each session to
    > the minimum values of relfrozenxid and relminmxid over all global
    > temporary tables in use by that session. Then, when VACUUM is run, it
    > sets pg_temp_class.relfrozenxid/relminmxid, based on the local
    > contents of the table, and pg_class.relfrozenxid/relminmxid taking
    > into account the contents of other sessions using global temporary
    > tables. It's a little crude, because it can't see other
    > relfrozenxid/relminmxid values on a per-table level for other
    > sessions, but this is sufficient to allow
    > pg_database.datfrozenxid/datminmxid to be advanced, provided that each
    > session runs VACUUM from time to time.
    >
    > This still suffers from the same problem as local temporary tables
    > though -- if a session uses a local or global temporary table, and
    > then just sits there, without ever running VACUUM, there is no way to
    > advance the pg_database fields, and eventually there will be a XID
    > wraparound danger. Autovacuum doesn't help, because it can't vacuum
    > temporary tables. It's not at all clear what can be done about that.
    > It might be of some help to add a diagnostic function to identify the
    > offending backend, though I have not done so here.
    >
    >
    > 0007 adds another global temporary system catalog table:
    > pg_temp_statistic. This has the exact same set of columns as
    > pg_statistic, but it is used to hold statistics about global temporary
    > tables (and again, it could in theory also be used for local temporary
    > tables).
    >
    > ANALYZE writes to pg_temp_statistic instead of pg_statistic for global
    > temporary tables, and various selectivity estimating functions are
    > updated to read from it, so each session gets its own local set of
    > per-column statistics for the global temporary tables that it uses.
    >
    > The pg_stats view is updated to a UNION ALL query selecting from
    > pg_statistic and pg_temp_statistic, so users can view their own
    > statistics data in the usual way.
    >
    >
    > 0008 adds pg_temp_statistic_ext_data, which is exactly the same as
    > pg_statistic_ext_data, except that it is a global temporary table used
    > to store extended statistics data for global temporary tables. The
    > views pg_stats_ext and pg_stats_ext_exprs are updated to include this.
    >
    >
    > 0009 adds a final global temporary system catalog table:
    > pg_temp_index. As noted in 0002, a session needs to be able to mark an
    > index on a global temporary table as invalid locally, if it was added
    > by another session after this session had already populated the table.
    > So pg_temp_index has indexrelid and indisvalid columns, so that the
    > valid state of an index can be overridden locally.
    >
    >
    > So far, I've focused on getting a set of patches that work, and these
    > do seem to operate as expected. However, it seems quite likely that
    > there are things that I have overlooked.
    >
    > I'm also aware that I haven't written any documentation yet, and I
    > need to add more tests, but as a rough set of patches, I hope that
    > they're in good enough shape for review.
    >
    
    Wow, we're on the same track. I have a patch series for exactly this 
    feature that I was about to submit.
    
    FTR here's where I'm at. I'll try to take a look at yours ASAP.
    
    
    
    cheers
    
    
    andrew
    
    
    
    --
    Andrew Dunstan
    EDB: https://www.enterprisedb.com
    
  3. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-06-22T08:56:54Z

    On Sun, 21 Jun 2026 at 23:06, Andrew Dunstan <andrew@dunslane.net> wrote:
    >
    > Wow, we're on the same track. I have a patch series for exactly this
    > feature that I was about to submit.
    >
    > FTR here's where I'm at. I'll try to take a look at yours ASAP.
    
    Oh, wow. There's a lot of similarity between our patchsets, which is
    reassuring, but there are also a number of differences, which I need
    to think about in more detail.
    
    In the meantime, here's a v2 of my patchset fixing up a few things
    noted by the cfbot, and one bug I spotted -- REPACK/VACUUM FULL on a
    global temporary table with associated toast tables failed if the
    table hadn't already been opened in the session, because the code in
    repack.c didn't open toast tables, so they were never being
    initialized.
    
    Regards,
    Dean
    
  4. Re: Global temporary tables

    Andrew Dunstan <andrew@dunslane.net> — 2026-06-22T13:43:00Z

    
    > On Jun 22, 2026, at 4:57 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    > 
    > On Sun, 21 Jun 2026 at 23:06, Andrew Dunstan <andrew@dunslane.net> wrote:
    >> 
    >> Wow, we're on the same track. I have a patch series for exactly this
    >> feature that I was about to submit.
    >> 
    >> FTR here's where I'm at. I'll try to take a look at yours ASAP.
    > 
    > Oh, wow. There's a lot of similarity between our patchsets, which is
    > reassuring, but there are also a number of differences, which I need
    > to think about in more detail.
    
    Yeah, agree on both fronts. The main areas I see are the catalog and handling wraparound. I don’t think there’s necessarily a clear winner on either, so I’ll be interested to hear what others say.
    
    Cheers
    
    Andrew
    
    
    
    
    
    
  5. Re: Global temporary tables

    Pavel Stehule <pavel.stehule@gmail.com> — 2026-06-22T15:30:59Z

    Hi
    
    po 22. 6. 2026 v 15:43 odesílatel Andrew Dunstan <andrew@dunslane.net>
    napsal:
    
    >
    >
    > > On Jun 22, 2026, at 4:57 AM, Dean Rasheed <dean.a.rasheed@gmail.com>
    > wrote:
    > >
    > > On Sun, 21 Jun 2026 at 23:06, Andrew Dunstan <andrew@dunslane.net>
    > wrote:
    > >>
    > >> Wow, we're on the same track. I have a patch series for exactly this
    > >> feature that I was about to submit.
    > >>
    > >> FTR here's where I'm at. I'll try to take a look at yours ASAP.
    > >
    > > Oh, wow. There's a lot of similarity between our patchsets, which is
    > > reassuring, but there are also a number of differences, which I need
    > > to think about in more detail.
    >
    > Yeah, agree on both fronts. The main areas I see are the catalog and
    > handling wraparound. I don’t think there’s necessarily a clear winner on
    > either, so I’ll be interested to hear what others say.
    >
    
    next week I'll have free time, I can check these patches
    
    generally - global temporary tables is super useful for migration from
    other systems
    
    Regards
    
    Pavel
    
    
    >
    > Cheers
    >
    > Andrew
    >
    >
    >
    >
    >
    
  6. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-06-23T09:12:43Z

    v3 attached, attempting to fix another issue reported by the cfbot --
    need to tolerate relation_open() on a global temporary relation, when
    in parallel mode because pg_get_viewdef() does that.
    
    Regards,
    Dean
    
  7. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-06-23T23:34:26Z

    On Tue, 23 Jun 2026 at 10:12, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    >
    > v3 attached, attempting to fix another issue reported by the cfbot --
    > need to tolerate relation_open() on a global temporary relation, when
    > in parallel mode because pg_get_viewdef() does that.
    
    Ugh, that still wasn't quite right. Here's v4 with more fixes for
    parallel workers.
    
    Regards,
    Dean
    
  8. Re: Global temporary tables

    Gilles DAROLD <gilles@darold.net> — 2026-06-24T04:15:21Z

    Le 24/06/2026 à 06:34, Dean Rasheed a écrit :
    > On Tue, 23 Jun 2026 at 10:12, Dean Rasheed<dean.a.rasheed@gmail.com> wrote:
    >> v3 attached, attempting to fix another issue reported by the cfbot --
    >> need to tolerate relation_open() on a global temporary relation, when
    >> in parallel mode because pg_get_viewdef() does that.
    > Ugh, that still wasn't quite right. Here's v4 with more fixes for
    > parallel workers.
    >
    > Regards,
    > Dean
    
    
    Congratulations, it's an ingenious implementation. Aside from 
    portability with other DBMSs, the fundamental advantage of GTTs is 
    avoiding catalog fragmentation caused by the intensive use of temporary 
    tables. This is what the patch looks to achieve.
    
    I will try to do more checks.
    
    Best regards.
    
    -- 
    Gilles Darold
    http://hexacluster.ai/
    
  9. Re: Global temporary tables

    Kirk Wolak <wolakk@gmail.com> — 2026-06-26T03:51:53Z

    On Tue, Jun 23, 2026 at 7:35 PM Dean Rasheed <dean.a.rasheed@gmail.com>
    wrote:
    
    > On Tue, 23 Jun 2026 at 10:12, Dean Rasheed <dean.a.rasheed@gmail.com>
    > wrote:
    > >
    > > v3 attached, attempting to fix another issue reported by the cfbot --
    > > need to tolerate relation_open() on a global temporary relation, when
    > > in parallel mode because pg_get_viewdef() does that.
    >
    > Ugh, that still wasn't quite right. Here's v4 with more fixes for
    > parallel workers.
    >
    > Regards,
    > Dean
    >
    + 1 for the idea.  I will try to review this over the weekend.  We could
    use this.
    
  10. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-06-26T19:37:07Z

    On Fri, 26 Jun 2026 at 04:52, Kirk Wolak <wolakk@gmail.com> wrote:
    >
    > + 1 for the idea.  I will try to review this over the weekend.  We could use this.
    
    Thanks. Any reviews will be very helpful.
    
    In the meantime, comparing the 2 patchsets, I realised that there was
    a bug in mine -- ON COMMIT DELETE ROWS wasn't working properly in all
    cases.
    
    More specifically, it was working for a global temporary table created
    in the current session, but disconnecting and then reconnecting caused
    it to stop working, because I hadn't realised that the ON COMMIT
    action of a temporary table is not saved to the database.
    
    Not saving the ON COMMIT action to the database kind-of made sense for
    old-style temporary tables, since they disappear at the end of the
    session. However, even then, it can be useful to have it in the
    database so that psql's \d meta-command can display it, and of course,
    for global temporary tables, saving it to the database is essential so
    that new sessions can pick it up.
    
    So here's a new patchset, where 0001 is new -- it saves a temporary
    table's ON COMMIT action to pg_class, and updates psql's \d to display
    it. I think we could commit that independently of the global temporary
    tables feature, since it seems somewhat useful by itself.
    
    (I chose to do it as a new column "reloncommit" in pg_class, rather
    than as a reloption, because a reloption didn't seem quite right for
    this, but that's a matter of opinion.)
    
    Regards,
    Dean
    
  11. Re: Global temporary tables

    Kirk Wolak <wolakk@gmail.com> — 2026-06-26T21:37:21Z

    On Fri, Jun 26, 2026 at 3:37 PM Dean Rasheed <dean.a.rasheed@gmail.com>
    wrote:
    
    > On Fri, 26 Jun 2026 at 04:52, Kirk Wolak <wolakk@gmail.com> wrote:
    > >
    > > + 1 for the idea.  I will try to review this over the weekend.  We could
    > use this.
    >
    > Thanks. Any reviews will be very helpful.
    >
    > In the meantime, comparing the 2 patchsets, I realised that there was
    > a bug in mine -- ON COMMIT DELETE ROWS wasn't working properly in all
    > cases.
    >
    > ...
    >
    > Not saving the ON COMMIT action to the database kind-of made sense for
    > old-style temporary tables, since they disappear at the end of the
    > session. However, even then, it can be useful to have it in the
    > database so that psql's \d meta-command can display it, and of course,
    > for global temporary tables, saving it to the database is essential so
    > that new sessions can pick it up.
    >
    
    Okay, a small "nit" on your wording.  No new "sessions" should see any data.
    They can't see that data, as it is not theirs.
    \d should see it.  Future selects should see it.
    In our case, we use this because we do a crap ton of work in temp tables
    (unlogged).
    And then we commit (along with the flags that indicate we are 1/2 done, and
    the timing logs).
    Then, we insert that into the table we want to see that data.
    Mark ourselves fully done.
    COMMIT.
    
    Temp data is still there, but then we disconnect and the table data goes
    away.
    
    ...
    >
    > Regards,
    > Dean
    >
    
    Kirk
    
  12. Re: Global temporary tables

    Pavel Stehule <pavel.stehule@gmail.com> — 2026-06-30T19:29:05Z

    Hi
    
    pá 26. 6. 2026 v 21:37 odesílatel Dean Rasheed <dean.a.rasheed@gmail.com>
    napsal:
    
    > On Fri, 26 Jun 2026 at 04:52, Kirk Wolak <wolakk@gmail.com> wrote:
    > >
    > > + 1 for the idea.  I will try to review this over the weekend.  We could
    > use this.
    >
    > Thanks. Any reviews will be very helpful.
    >
    > In the meantime, comparing the 2 patchsets, I realised that there was
    > a bug in mine -- ON COMMIT DELETE ROWS wasn't working properly in all
    > cases.
    >
    > More specifically, it was working for a global temporary table created
    > in the current session, but disconnecting and then reconnecting caused
    > it to stop working, because I hadn't realised that the ON COMMIT
    > action of a temporary table is not saved to the database.
    >
    > Not saving the ON COMMIT action to the database kind-of made sense for
    > old-style temporary tables, since they disappear at the end of the
    > session. However, even then, it can be useful to have it in the
    > database so that psql's \d meta-command can display it, and of course,
    > for global temporary tables, saving it to the database is essential so
    > that new sessions can pick it up.
    >
    > So here's a new patchset, where 0001 is new -- it saves a temporary
    > table's ON COMMIT action to pg_class, and updates psql's \d to display
    > it. I think we could commit that independently of the global temporary
    > tables feature, since it seems somewhat useful by itself.
    >
    > (I chose to do it as a new column "reloncommit" in pg_class, rather
    > than as a reloption, because a reloption didn't seem quite right for
    > this, but that's a matter of opinion.)
    >
    
    I am testing this patchset. Generally, it looks very well. I didn't see any
    bloat on catalog
    
    I found an issue - TRUNCATE waits when gtt is used by different
    transactions, and TRUNCATE removes content in all sessions.
    
    Is it expected?
    
    Regards
    
    Pavel
    
    
    >
    > Regards,
    > Dean
    >
    
  13. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-06-30T20:21:27Z

    On Tue, 30 Jun 2026 at 20:29, Pavel Stehule <pavel.stehule@gmail.com> wrote:
    >
    > I am testing this patchset. Generally, it looks very well. I didn't see any bloat on catalog
    >
    > I found an issue - TRUNCATE waits when gtt is used by different transactions, and TRUNCATE removes content in all sessions.
    >
    > Is it expected?
    
    Thanks for testing!
    
    The waiting issue for TRUNCATE is interesting. I hadn't considered
    that at all, so TRUNCATE on a global temporary table still takes an
    access exclusive lock, like any other table, but I guess that's not
    necessary. I wonder what lock level we could reduce it to.
    
    I can't reproduce TRUNCATE removing content in all sessions, but
    that's definitely not supposed to happen. Can you share what you did
    to cause that?
    
    Regards,
    Dean
    
    
    
    
  14. Re: Global temporary tables

    Pavel Stehule <pavel.stehule@gmail.com> — 2026-06-30T20:32:19Z

    út 30. 6. 2026 v 22:21 odesílatel Dean Rasheed <dean.a.rasheed@gmail.com>
    napsal:
    
    > On Tue, 30 Jun 2026 at 20:29, Pavel Stehule <pavel.stehule@gmail.com>
    > wrote:
    > >
    > > I am testing this patchset. Generally, it looks very well. I didn't see
    > any bloat on catalog
    > >
    > > I found an issue - TRUNCATE waits when gtt is used by different
    > transactions, and TRUNCATE removes content in all sessions.
    > >
    > > Is it expected?
    >
    > Thanks for testing!
    >
    > The waiting issue for TRUNCATE is interesting. I hadn't considered
    > that at all, so TRUNCATE on a global temporary table still takes an
    > access exclusive lock, like any other table, but I guess that's not
    > necessary. I wonder what lock level we could reduce it to.
    >
    > I can't reproduce TRUNCATE removing content in all sessions, but
    > that's definitely not supposed to happen. Can you share what you did
    > to cause that?
    >
    >
    I cannot reproduce it now. I was confused. I am sorry for the noise.
    
    Regards
    
    Pavel
    
    
    
    
    
    > Regards,
    > Dean
    >
    
  15. Re: Global temporary tables

    Haibo Yan <tristan.yim@gmail.com> — 2026-06-30T21:41:08Z

    On Fri, Jun 26, 2026 at 12:37 PM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    >
    > On Fri, 26 Jun 2026 at 04:52, Kirk Wolak <wolakk@gmail.com> wrote:
    > >
    > > + 1 for the idea.  I will try to review this over the weekend.  We could use this.
    >
    > Thanks. Any reviews will be very helpful.
    >
    > In the meantime, comparing the 2 patchsets, I realised that there was
    > a bug in mine -- ON COMMIT DELETE ROWS wasn't working properly in all
    > cases.
    >
    > More specifically, it was working for a global temporary table created
    > in the current session, but disconnecting and then reconnecting caused
    > it to stop working, because I hadn't realised that the ON COMMIT
    > action of a temporary table is not saved to the database.
    >
    > Not saving the ON COMMIT action to the database kind-of made sense for
    > old-style temporary tables, since they disappear at the end of the
    > session. However, even then, it can be useful to have it in the
    > database so that psql's \d meta-command can display it, and of course,
    > for global temporary tables, saving it to the database is essential so
    > that new sessions can pick it up.
    >
    > So here's a new patchset, where 0001 is new -- it saves a temporary
    > table's ON COMMIT action to pg_class, and updates psql's \d to display
    > it. I think we could commit that independently of the global temporary
    > tables feature, since it seems somewhat useful by itself.
    >
    > (I chose to do it as a new column "reloncommit" in pg_class, rather
    > than as a reloption, because a reloption didn't seem quite right for
    > this, but that's a matter of opinion.)
    >
    > Regards,
    > Dean
    
    I did another round of testing on the v5 GTT patch set, based on
    commit 57f19774d6c. The build used --enable-cassert --enable-debug
    --enable-tap-tests.
    
    The additional tests covered:
    --------------------------------------------------------------------------------
    pg_dump / pg_restore
    pg_basebackup / restore
    permissions around pg_temp_statistic / pg_all_statistic
    deeper partitioned GTT cases
    DDL while another session is using the GTT
    concurrent stress with multiple sessions
    --------------------------------------------------------------------------------
    All of those tests passed. I did not find correctness bugs in these areas.
    
    A few items still seem worth addressing:
    1. There is no SQL-visible diagnostic for PGPROC.tempfrozenxid / tempminmxid.
    If an idle backend that used a GTT prevents datfrozenxid advancement,
    it does not seem easy for a DBA to identify the backend from SQL. It
    would be useful to expose this, either in pg_stat_activity or in a
    small diagnostic view/function.
    
    2. The base-vs-effective metadata model should be documented.
    For GTTs, direct pg_class / pg_index reads may show base metadata,
    while pg_temp_class / pg_temp_index, pg_relation_filenode(), psql \d,
    and the planner use current-session effective metadata. Related DDL
    behavior is also subtle: some operations affect only the current
    session’s local storage, while rewrite operations may be rejected if
    another session is using the GTT. This seems reasonable, but it should
    be documented.
    
    3. CREATE INDEX CONCURRENTLY on a GTT needs clarification.
    The current behavior appears to follow the local-temp-table path and
    treat CIC as a normal index build. That may be OK, since only the
    current session’s local storage is built, but the nearby comment about
    other backends not accessing the relation is misleading for GTTs. I
    slightly lean toward rejecting CIC on GTTs explicitly, unless we want
    to document that GTTs intentionally follow local temporary table
    behavior here.
    
    Minor cleanup:
    describe.c still has FIXME: needs to be PG20 version checks using 190000.
    
    Overall, the patch looks much more robust than I initially expected.
    My remaining concerns are mostly diagnostics and documentation rather
    than correctness bugs.
    
    Thanks
    
    Regards
    Haibo
    
    
    
    
  16. Re: Global temporary tables

    Japin Li <japinli@hotmail.com> — 2026-07-01T06:24:46Z

    Hi, Dean
    
    Thanks for working on this.
    
    On Fri, 26 Jun 2026 at 20:37, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    > On Fri, 26 Jun 2026 at 04:52, Kirk Wolak <wolakk@gmail.com> wrote:
    >>
    >> + 1 for the idea.  I will try to review this over the weekend.  We could use this.
    >
    > Thanks. Any reviews will be very helpful.
    >
    > In the meantime, comparing the 2 patchsets, I realised that there was
    > a bug in mine -- ON COMMIT DELETE ROWS wasn't working properly in all
    > cases.
    >
    > More specifically, it was working for a global temporary table created
    > in the current session, but disconnecting and then reconnecting caused
    > it to stop working, because I hadn't realised that the ON COMMIT
    > action of a temporary table is not saved to the database.
    >
    > Not saving the ON COMMIT action to the database kind-of made sense for
    > old-style temporary tables, since they disappear at the end of the
    > session. However, even then, it can be useful to have it in the
    > database so that psql's \d meta-command can display it, and of course,
    > for global temporary tables, saving it to the database is essential so
    > that new sessions can pick it up.
    >
    > So here's a new patchset, where 0001 is new -- it saves a temporary
    > table's ON COMMIT action to pg_class, and updates psql's \d to display
    > it. I think we could commit that independently of the global temporary
    > tables feature, since it seems somewhat useful by itself.
    >
    > (I chose to do it as a new column "reloncommit" in pg_class, rather
    > than as a reloption, because a reloption didn't seem quite right for
    > this, but that's a matter of opinion.)
    >
    
    While testing the v5 patch, I encountered a lock wait.
    
    2026-07-01 14:18:43.603 CST [1486593] LOG:  process 1486593 still waiting for AccessExclusiveLock on relation 16384 of database 5 after 1000.106 ms
    2026-07-01 14:18:43.603 CST [1486593] DETAIL:  Process holding the lock: 1486484. Wait queue: 1486593.
    2026-07-01 14:18:43.603 CST [1486593] CONTEXT:  waiting for AccessExclusiveLock on relation 16384 of database 5
    2026-07-01 14:18:43.603 CST [1486593] STATEMENT:  SELECT * FROM gtt_delete;
    
    To reproduce:
    
    Session 1:
    
    CREATE GLOBAL TEMPORARY TABLE gtt_delete (id int) ON COMMIT DELETE ROWS;
    BEGIN;
    INSERT INTO gtt_delete VALUES (1);
    
    Session 2:
    
    SELECT * FROM gtt_delete;   ---> blocked
    
    Is this expected?
    
    > Regards,
    > Dean
    
    -- 
    Regards,
    Japin Li
    ChengDu WenWu Information Technology Co., Ltd.
    
    
    
    
  17. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-07-01T07:28:07Z

    On Tue, 30 Jun 2026 at 22:41, Haibo Yan <tristan.yim@gmail.com> wrote:
    >
    > I did another round of testing on the v5 GTT patch set, based on
    > commit 57f19774d6c. The build used --enable-cassert --enable-debug
    > --enable-tap-tests.
    
    Thank you for testing!
    
    > Overall, the patch looks much more robust than I initially expected.
    > My remaining concerns are mostly diagnostics and documentation rather
    > than correctness bugs.
    
    I agree with all your points. I'll try to post an update later this week.
    
    Regards,
    Dean
    
    
    
    
  18. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-07-01T07:32:37Z

    On Wed, 1 Jul 2026 at 07:24, Japin Li <japinli@hotmail.com> wrote:
    >
    > Thanks for working on this.
    
    Thank you for testing it!
    
    > While testing the v5 patch, I encountered a lock wait.
    >
    > 2026-07-01 14:18:43.603 CST [1486593] LOG:  process 1486593 still waiting for AccessExclusiveLock on relation 16384 of database 5 after 1000.106 ms
    > 2026-07-01 14:18:43.603 CST [1486593] DETAIL:  Process holding the lock: 1486484. Wait queue: 1486593.
    > 2026-07-01 14:18:43.603 CST [1486593] CONTEXT:  waiting for AccessExclusiveLock on relation 16384 of database 5
    > 2026-07-01 14:18:43.603 CST [1486593] STATEMENT:  SELECT * FROM gtt_delete;
    >
    > Is this expected?
    
    Ah yes, you're right. That's not good.
    
    The root cause looks to be the same issue that Pavel reported -- the
    ON COMMIT DELETE ROWS does a TRUNCATE when the transaction is
    committed, which requires an AccessExclusiveLock. I think the patch
    should reduce the lock level required for TRUNCATE on a global
    temporary relation to RowExclusiveLock, like DELETE.
    
    I'll try to post an update later this week.
    
    Regards,
    Dean
    
    
    
    
  19. Re: Global temporary tables

    Nico Williams <nico@cryptonector.com> — 2026-07-01T23:20:23Z

    On Fri, Jun 26, 2026 at 08:37:07PM +0100, Dean Rasheed wrote:
    > Thanks. Any reviews will be very helpful.
    
    I drove Claude through a review.  You can too, so there's nothing
    special to it, but I'll go over it here, with a bit of my content added,
    and hopefully y'all don't hate me for sharing LLM outputs.
    
    1. You have an uninitialized have_gtrs in
       src/backend/commands/vacuum.c:504.
    
    2. TRUNCATE on a GTT can cause corruption.
    
       Claude's description (I don't know how to evaluate this one myself):
    
       RelationSetNewRelfilenumber writes session-local freeze xids into
       shared pg_class for GTTs (relcache.c:4057-4076). Only relfilenode is
       split via SetEffective_relfilenode. relpages=0, reltuples=-1,
       relallvisible=0, relallfrozen=0, relfrozenxid=freezeXid,
       relminmxid=minmulti, relpersistence are written directly into
       classform and committed via CatalogTupleUpdate(pg_class, ...). Called
       for GTT TRUNCATE (tablecmds.c:2301) and REINDEX (index.c:3908). For a
       GTT, the new freezeXid reflects this session's freshly created
       storage; clobbering shared pg_class.relfrozenxid with it can advance
       the global horizon past what other sessions' local storage actually
       has.
    
       Exploring further Claude added:
    
       The failure chain:
    
       1. t=0: Session B opens gtt_foo, MyProc->tempfrozenxid = X0 (~old
          RecentXmin). B never inserts anything; its local file has no rows
          but is still bound to freezeXid X0.
       2. t=1: Session A opens gtt_foo, inserts rows in xids X0…X1, commits.
          A's local storage holds rows with xmin in that range. B's local
          file is untouched.
       3. t=2: Session A: TRUNCATE gtt_foo;. RelationSetNewRelfilenumber
          runs. table_relation_set_new_filelocator returns freezeXid = X2
          (~current). Code writes pg_class.relfrozenxid = X2 — no cap.
       4. t=3: Any session VACUUMs any tables. vac_update_datfrozenxid walks
          pg_class, sees gtt_foo.relfrozenxid = X2. If other pg_class rows
          all have similar or newer values, newFrozenXid ≈ X2.
       5. t=4: dbform->datfrozenxid = X2. vac_truncate_clog(X2, ...) at
          :2019 truncates CLOG for everything older than X2.
       6. t=5: Session B, whose local gtt_foo file was actually created back
          at t=0 with freezeXid X0, has no problem itself (empty file). But
          suppose C is any other backend also holding local storage with
          xids older than X2 (any GTT, or a running long snapshot's stale
          hint-bit-less pages) — reading such rows now does
          TransactionIdDidCommit(x) → CLOG lookup → "ERROR: could not access
          status of transaction x".
       7. Worse: because the whole cluster's CLOG is truncated, this is not
          just B's GTT. Any tuple anywhere the cluster still holds with an
          xid older than X2 that lacks the HEAP_XMIN_COMMITTED hint bit
          becomes unreadable. TOAST chains, index-only scan hints — all
          vulnerable to torn interpretation.
    
       Fix: the post-SetEffective_relfilenode block must update
       temp_classform instead of classform when temp_tuple is valid (or skip
       stats/freeze fields on classform entirely for GTT).
    
    3. vac_get_min_tempfrozenxids() scans ProcGlobal->allProcCount without
       ProcArrayLock (vacuum.c:1769-1799).  Every comparable consumer
       (vac_update_datfrozenxid, GetSnapshotData) acquires it.
    
       Without this lock, a backend can exit mid-scan and a new backend
       reuse the slot; you'll read databaseId/tempfrozenxid from
       inconsistent slot generations.  This can lead to corruption.
    
    4. eoxact_usage_list_len / eoxact_usage_list_overflowed never reset at
       end-of-xact (global_temp.c:1176-1179). Only the storage list counters
       reset.  Once a long-lived session overflows (>32 GTTs touched
       cumulatively), every subsequent xact falls back to full hash_seq
       scans.  Correctness preserved, performance silently degrades.
    
    5. MyProc->tempfrozenxid never decays. Set on first GTT access; only
       moves forward on a session's VACUUM of its own GTT. A backend that
       touches a GTT once and never vacuums holds back pg_class.relfrozenxid
       advancement for all sessions for the connection's lifetime. No path
       clears it on GlobalTempRelationDropped or when local usage_count
       drops to zero.
    
       I explored this one a bit because I was surprised that the frozenxid
       in the catalog should be relevant to GTTs.  If I understand correctly
       this tempfrozenxid exists because while the GTT storage is
       per-session, there is some coupling via the CLOG, but I don't
       understand why _that_ has to be, and this is what Claude told me:
    
         An alternative that would have avoided the coordination
    
         You could have designed it so that GTT pg_class rows are skipped
         from datfrozenxid computation (just like local temp), and each
         backend's tempfrozenxid fed directly into vac_update_datfrozenxid
         as an additional constraint. That would let the shared
         pg_class.relfrozenxid be advisory/stale without a safety issue,
         removing one leg of the coordination.
    
         The current design chose to keep the shared row accurate — probably
         because it's user-visible (SELECT relfrozenxid FROM pg_class) and
         drives per-relation autovacuum urgency signals. That's the
         tradeoff: an accurate shared row, in exchange for needing
         cross-session horizon exchange. Same fundamental need as local
         temp; different plumbing because the pg_class row is shared.
    
       And, ah, well, I guess you could say that `relfrozenxid` in
       `pg_class` is not meaningful for GTTs, or that you'll get your
       session's GTT's relfrozenxid.
    
    6. InsertPgTempIndexTuple mutates rel->rd_index->indisvalid in-place
       (global_temp.c:885-892).  Relcache fields are normally immutable
       outside relcache.c.  Works today but fragile.
    
    7. AbortTransaction ordering — gtrs_dropped is list_freed
       unconditionally in AtEOXact (:1178) even though the in-source comment
       (:1077-1080) claims it'll be "repeated next commit". Mostly benign
       (storage is reaped at exit) but the comment is wrong.
    
    8. DeleteRelationTuple calls GlobalTempRelationDropped after
       CatalogTupleDelete; GlobalTempRelationDropped then calls
       get_rel_relkind(relid) (global_temp.c:1038). After a CCI, the
       syscache will return '\0' and skip DeletePgTempIndexTuple. Pass
       relkind explicitly instead of re-deriving it.
    
    Nico
    -- 
    
    
    
    
  20. Re: Global temporary tables

    Japin Li <japinli@hotmail.com> — 2026-07-02T09:11:35Z

    Hi, Dean
    
    On Wed, 01 Jul 2026 at 08:32, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    > On Wed, 1 Jul 2026 at 07:24, Japin Li <japinli@hotmail.com> wrote:
    >>
    >> Thanks for working on this.
    >
    > Thank you for testing it!
    >
    >> While testing the v5 patch, I encountered a lock wait.
    >>
    >> 2026-07-01 14:18:43.603 CST [1486593] LOG:  process 1486593 still waiting for AccessExclusiveLock on relation 16384 of database 5 after 1000.106 ms
    >> 2026-07-01 14:18:43.603 CST [1486593] DETAIL:  Process holding the lock: 1486484. Wait queue: 1486593.
    >> 2026-07-01 14:18:43.603 CST [1486593] CONTEXT:  waiting for AccessExclusiveLock on relation 16384 of database 5
    >> 2026-07-01 14:18:43.603 CST [1486593] STATEMENT:  SELECT * FROM gtt_delete;
    >>
    >> Is this expected?
    >
    > Ah yes, you're right. That's not good.
    >
    > The root cause looks to be the same issue that Pavel reported -- the
    > ON COMMIT DELETE ROWS does a TRUNCATE when the transaction is
    > committed, which requires an AccessExclusiveLock. I think the patch
    > should reduce the lock level required for TRUNCATE on a global
    > temporary relation to RowExclusiveLock, like DELETE.
    >
    
    Here is my review on v5-0002.  Please take a look.
    
    1.
    $ git am ~/Patches/gtt/*.patch
    Applying: Save temporary table ON COMMIT actions to pg_class.
    Applying: Basic support for global temporary tables.
    Applying: Support indexes on global temporary tables.
    Applying: Support global temporary sequences.
    Applying: Support global temporary catalog tables and add pg_temp_class.
    Applying: Add relation statistics columns to pg_temp_class.
    Applying: Add relfrozenxid and relminmxid columns to pg_temp_class.
    .git/rebase-apply/patch:940: new blank line at EOF.
    +
    warning: 1 line adds whitespace errors.
    Applying: Add pg_temp_statistic global temporary catalog table.
    Applying: Add pg_temp_statistic_ext_data global temporary catalog table.
    Applying: Add pg_temp_index global temporary catalog table.
    
    2.
    +static void
    +gtr_delete_all_storage_on_exit(int code, Datum arg)
    +{
    +	ProcNumber	backend;
    +	HASH_SEQ_STATUS status;
    +	GtrStorageEntry *entry;
    +	SMgrRelation srel;
    +
    +	/* Loop over all storage created and delete it */
    +	backend = ProcNumberForTempRelations();
    +	hash_seq_init(&status, gtr_local_storage);
    +	while ((entry = hash_seq_search(&status)) != NULL)
    +	{
    +		srel = smgropen(entry->rlocator, backend);
    +		smgrdounlinkall(&srel, 1, false);
    +		smgrclose(srel);
    +	}
    +}
    
    We can restrict srel to the loop scope.
    
    3.
    +gtr_remove_all_usage_on_exit(int code, Datum arg)
    +{
    +	HASH_SEQ_STATUS status;
    +	GtrUsageEntry *local_entry;
    +	GtrSharedUsageKey key;
    +	GtrSharedUsageEntry *shared_entry;
    +
    +	/* Loop over all the global temporary relations we were using */
    +	hash_seq_init(&status, gtr_local_usage);
    +	while ((local_entry = hash_seq_search(&status)) != NULL)
    +	{
    +		/* Find the shared usage entry */
    +		key.dbid = MyDatabaseId;
    +		key.relid = local_entry->relid;
    +		shared_entry = dshash_find(gtr_shared_usage, &key, true);
    
    Same as above: move the declarations of key and shared_entry inside the loop.
    
    4.
    +static void
    +gtr_init_usage_tables(void)
    +{
    +	HASHCTL		ctl;
    +	MemoryContext oldcontext;
    +
    +	/* Local usage table */
    +	if (gtr_local_usage == NULL)
    +	{
    +		ctl.keysize = sizeof(Oid);
    +		ctl.entrysize = sizeof(GtrUsageEntry);
    +
    +		gtr_local_usage = hash_create("Global temporary relations in use locally",
    +									  128, &ctl, HASH_ELEM | HASH_BLOBS);
    +	}
    +
    +	/* Shared usage table */
    +	if (gtr_shared_usage == NULL)
    +	{
    +		/* Use a lock to ensure only one process creates the table */
    +		LWLockAcquire(GlobalTempRelControlLock, LW_EXCLUSIVE);
    +
    +		/* Be sure any local memory allocated by DSA routines is persistent */
    +		oldcontext = MemoryContextSwitchTo(TopMemoryContext);
    
    Likewise, narrow ctl to the first if and oldContext to the second.
    
    5.
    +	 * the usage hash table entries for it.  Otherwise, reset the hash entry's
    +	 * subids to zero.
    
    Instead of zero, how about using an invalid sub-transaction ID?
    
    6.
    +static void
    +gtr_process_invalidated_gtrs(void)
    +{
    +	MemoryContext oldcontext;
    +	Oid			relid;
    +
    +	/*
    +	 * Scan the gtrs_invalidated list and add any dropped relations to the
    +	 * gtrs_dropped list.  Since the transaction might fail later on, we need
    +	 * the gtrs_dropped list to persist until we can successfully process it.
    +	 */
    +	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
    +
    +	/*
    +	 * As we scan the gtrs_invalidated list, more invalidation messages might
    +	 * arrive, so keep going until it is empty.
    +	 */
    +	while (gtrs_invalidated != NIL)
    +	{
    +		/* Pop the last relid from the list */
    +		relid = llast_oid(gtrs_invalidated);
    
    Restrict relid to the while loop scope.
    
    7.
    +void
    +TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
    +							   ProcNumber backend, bool create)
    +{
    +	GtrStorageEntry *entry;
    +	bool		found;
    +	SMgrRelation srel;
    +
    +	if (create)
    +	{
    +		/* Initialize the storage table, if necessary */
    +		gtr_init_storage_table();
    +
    +		/* Insert an entry to track the storage */
    +		entry = hash_search(gtr_local_storage, &rlocator, HASH_ENTER, &found);
    
    Likewise, restrict found and srel to the if statement.
    
    8.
    +		/* Register the ON COMMIT action for relation, if it's DELETE ROWS */
    +		Assert(relation->rd_rel->reloncommit == RELONCOMMIT_PRESERVE_ROWS ||
    +			   relation->rd_rel->reloncommit == RELONCOMMIT_DELETE_ROWS);
    +
    +		if (relation->rd_rel->reloncommit == RELONCOMMIT_DELETE_ROWS)
    +			register_on_commit_action(relation->rd_id, ONCOMMIT_DELETE_ROWS);
    
    How about moving the comment to just before the if statement?
    
    9.
    +void
    +InvalidateGlobalTempRelation(Oid relid)
    +{
    +	MemoryContext oldcontext;
    +	HASH_SEQ_STATUS status;
    +	GtrUsageEntry *entry;
    +
    +	/* Quick exit if we haven't used any global temporary relations */
    +	if (gtr_local_usage == NULL)
    +		return;
    
    Restrict status and entry to the else statement.
    
    10.
    +void
    +GlobalTempRelationDropped(Oid relid)
    
    Not a fan of this name, but no better idea yet.
    
    11.
    +void
    +PreCommit_GlobalTempRelation(void)
    +{
    +	HASH_SEQ_STATUS status;
    +	GtrStorageEntry *storage_entry;
    +	Relation	rel;
    +
    
    Restrict rel to the while loop.
    
    12.
    +List *
    +AllGlobalTempRelationsInUse(Oid dbid)
    
    How about GetGlobalTempReplationsInUse()?
    
    13.
    +				ereport(ERROR,
    +						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
    +						 errmsg("cannot create relations in temporary schemas of other sessions")));
    
    For consistency with the above, I suggest changing the error message to:
    
        cannot create global temporary relation in temporary schema of other sessions
    
    14.
    +         * If they're global temp relations, reassign rel2's storage to rel1.
    
    We use "global temporary relations" elsewhere – should we keep the same
    terminology here?
    
    15.
    +                     errmsg(RELATION_IS_GLOBAL_TEMP(relation)
    +                            ? "cannot create a local temporary relation as partition of global temporary relation \"%s\""
    +                            : "cannot create a temporary relation as partition of permanent relation \"%s\"",
    
    Should we use the following error message:
    
        cannot create a local temporary relation as partition of permanent relation \"%s\"
    
    16.
    +                            : relpersistence == RELPERSISTENCE_GLOBAL_TEMP
    +                            ? "cannot create a global temporary relation as partition of local temporary relation \"%s\""
                                 : "cannot create a permanent relation as partition of temporary relation \"%s\"",
    
    Same as above:
    
        cannot create a permanent relation as partition of local temporary relation \"%s\"
    
    17.
    +            if (RELATION_IS_GLOBAL_TEMP(OldHeap) &&
    +                IsOtherUsingGlobalTempRelation(RelationGetRelid(OldHeap)))
    +                ereport(ERROR,
    +                        errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +                        errmsg("cannot rewrite global temporary table \"%s\" because it is being used in another session",
    +                               RelationGetRelationName(OldHeap)));
    
    
    18.
    +                            errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +                            errmsg("cannot add or alter constraints of global temporary table \"%s\" because it is being used in another session",
    +                                   get_rel_name(tab->relid)));
    
    19.
     			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
     				ereport(ERROR,
     						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
    -						 errmsg("constraints on temporary tables may reference only temporary tables")));
    +						 errmsg("constraints on local temporary tables may reference only local temporary tables")));
     			if (!pkrel->rd_islocaltemp || !rel->rd_islocaltemp)
     				ereport(ERROR,
     						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
    -						 errmsg("constraints on temporary tables must involve temporary tables of this session")));
    +						 errmsg("constraints on local temporary tables must involve local temporary tables of this session")));
    +			break;
    +		case RELPERSISTENCE_GLOBAL_TEMP:
    +			if (!RELATION_IS_GLOBAL_TEMP(pkrel))
    +				ereport(ERROR,
    +						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
    +						 errmsg("constraints on global temporary tables may reference only global temporary tables")));
    
    s/global temporary table/global temporary relation/g
    s/local temporary table/local temporary relation/g
    
    20.
     		attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
     		ereport(ERROR,
     				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
    -				 errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"",
    +				 errmsg(RELATION_IS_GLOBAL_TEMP(rel)
    +						? "cannot attach a local temporary relation as partition of global temporary relation \"%s\""
    +						: "cannot attach a temporary relation as partition of permanent relation \"%s\"",
     						RelationGetRelationName(rel))));
    
    Same as 15:
    
        cannot attach a local temporary relation as partition of permanent relation \"%s\"
    
    21.
     		attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
     		ereport(ERROR,
     				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
    -				 errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
    +				 errmsg(RELATION_IS_GLOBAL_TEMP(attachrel)
    +						? "cannot attach a global temporary relation as partition of local temporary relation \"%s\""
    +						: "cannot attach a permanent relation as partition of temporary relation \"%s\"",
     						RelationGetRelationName(rel))));
    
    Same as 16:
    
        cannot attach a permanent relation as partition of local temporary relation \"%s\
    
    22.
    @@ -22776,7 +22828,9 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
     		newPartName->relpersistence == RELPERSISTENCE_TEMP)
     		ereport(ERROR,
     				errcode(ERRCODE_WRONG_OBJECT_TYPE),
    -				errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
    +				errmsg(parent_relform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP
    +					   ? "cannot create a local temporary relation as partition of global temporary relation \"%s\""
    +					   : "cannot create a temporary relation as partition of permanent relation \"%s\"",
     					   RelationGetRelationName(parent_rel)));
     
     	/* Permanent rels cannot be partitions belonging to a temporary parent. */
    
    Suggestion:
    
        cannot create a local temporary relation as partition of permanent relation \"%s\"
    
    23.
    @@ -22784,7 +22838,9 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
     		parent_relform->relpersistence == RELPERSISTENCE_TEMP)
     		ereport(ERROR,
     				errcode(ERRCODE_WRONG_OBJECT_TYPE),
    -				errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
    +				errmsg(newPartName->relpersistence == RELPERSISTENCE_GLOBAL_TEMP
    +					   ? "cannot create a global temporary relation as partition of local temporary relation \"%s\""
    +					   : "cannot create a permanent relation as partition of temporary relation \"%s\"",
     					   RelationGetRelationName(parent_rel)));
    
    Suggest:
    
        cannot create a permanent relation as partition of local temporary relation \"%s\"
    
    
    -- 
    Regards,
    Japin Li
    ChengDu WenWu Information Technology Co., Ltd.
    
    
    
    
  21. Re: Global temporary tables

    Japin Li <japinli@hotmail.com> — 2026-07-03T03:16:57Z

    Hi, Dean
    
    On Thu, 02 Jul 2026 at 17:11, Japin Li <japinli@hotmail.com> wrote:
    > Hi, Dean
    >
    > On Wed, 01 Jul 2026 at 08:32, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    >> On Wed, 1 Jul 2026 at 07:24, Japin Li <japinli@hotmail.com> wrote:
    >>>
    >>> Thanks for working on this.
    >>
    >> Thank you for testing it!
    >>
    >>> While testing the v5 patch, I encountered a lock wait.
    >>>
    >>> 2026-07-01 14:18:43.603 CST [1486593] LOG:  process 1486593 still waiting for AccessExclusiveLock on relation 16384 of database 5 after 1000.106 ms
    >>> 2026-07-01 14:18:43.603 CST [1486593] DETAIL:  Process holding the lock: 1486484. Wait queue: 1486593.
    >>> 2026-07-01 14:18:43.603 CST [1486593] CONTEXT:  waiting for AccessExclusiveLock on relation 16384 of database 5
    >>> 2026-07-01 14:18:43.603 CST [1486593] STATEMENT:  SELECT * FROM gtt_delete;
    >>>
    >>> Is this expected?
    >>
    >> Ah yes, you're right. That's not good.
    >>
    >> The root cause looks to be the same issue that Pavel reported -- the
    >> ON COMMIT DELETE ROWS does a TRUNCATE when the transaction is
    >> committed, which requires an AccessExclusiveLock. I think the patch
    >> should reduce the lock level required for TRUNCATE on a global
    >> temporary relation to RowExclusiveLock, like DELETE.
    >>
    >
    > Here is my review on v5-0002.  Please take a look.
    >
    > 1.
    > $ git am ~/Patches/gtt/*.patch
    > Applying: Save temporary table ON COMMIT actions to pg_class.
    > Applying: Basic support for global temporary tables.
    > Applying: Support indexes on global temporary tables.
    > Applying: Support global temporary sequences.
    > Applying: Support global temporary catalog tables and add pg_temp_class.
    > Applying: Add relation statistics columns to pg_temp_class.
    > Applying: Add relfrozenxid and relminmxid columns to pg_temp_class.
    > .git/rebase-apply/patch:940: new blank line at EOF.
    > +
    > warning: 1 line adds whitespace errors.
    > Applying: Add pg_temp_statistic global temporary catalog table.
    > Applying: Add pg_temp_statistic_ext_data global temporary catalog table.
    > Applying: Add pg_temp_index global temporary catalog table.
    >
    > 2.
    > +static void
    > +gtr_delete_all_storage_on_exit(int code, Datum arg)
    > +{
    > +	ProcNumber	backend;
    > +	HASH_SEQ_STATUS status;
    > +	GtrStorageEntry *entry;
    > +	SMgrRelation srel;
    > +
    > +	/* Loop over all storage created and delete it */
    > +	backend = ProcNumberForTempRelations();
    > +	hash_seq_init(&status, gtr_local_storage);
    > +	while ((entry = hash_seq_search(&status)) != NULL)
    > +	{
    > +		srel = smgropen(entry->rlocator, backend);
    > +		smgrdounlinkall(&srel, 1, false);
    > +		smgrclose(srel);
    > +	}
    > +}
    >
    > We can restrict srel to the loop scope.
    >
    > 3.
    > +gtr_remove_all_usage_on_exit(int code, Datum arg)
    > +{
    > +	HASH_SEQ_STATUS status;
    > +	GtrUsageEntry *local_entry;
    > +	GtrSharedUsageKey key;
    > +	GtrSharedUsageEntry *shared_entry;
    > +
    > +	/* Loop over all the global temporary relations we were using */
    > +	hash_seq_init(&status, gtr_local_usage);
    > +	while ((local_entry = hash_seq_search(&status)) != NULL)
    > +	{
    > +		/* Find the shared usage entry */
    > +		key.dbid = MyDatabaseId;
    > +		key.relid = local_entry->relid;
    > +		shared_entry = dshash_find(gtr_shared_usage, &key, true);
    >
    > Same as above: move the declarations of key and shared_entry inside the loop.
    >
    > 4.
    > +static void
    > +gtr_init_usage_tables(void)
    > +{
    > +	HASHCTL		ctl;
    > +	MemoryContext oldcontext;
    > +
    > +	/* Local usage table */
    > +	if (gtr_local_usage == NULL)
    > +	{
    > +		ctl.keysize = sizeof(Oid);
    > +		ctl.entrysize = sizeof(GtrUsageEntry);
    > +
    > +		gtr_local_usage = hash_create("Global temporary relations in use locally",
    > +									  128, &ctl, HASH_ELEM | HASH_BLOBS);
    > +	}
    > +
    > +	/* Shared usage table */
    > +	if (gtr_shared_usage == NULL)
    > +	{
    > +		/* Use a lock to ensure only one process creates the table */
    > +		LWLockAcquire(GlobalTempRelControlLock, LW_EXCLUSIVE);
    > +
    > +		/* Be sure any local memory allocated by DSA routines is persistent */
    > +		oldcontext = MemoryContextSwitchTo(TopMemoryContext);
    >
    > Likewise, narrow ctl to the first if and oldContext to the second.
    >
    > 5.
    > +	 * the usage hash table entries for it.  Otherwise, reset the hash entry's
    > +	 * subids to zero.
    >
    > Instead of zero, how about using an invalid sub-transaction ID?
    >
    > 6.
    > +static void
    > +gtr_process_invalidated_gtrs(void)
    > +{
    > +	MemoryContext oldcontext;
    > +	Oid			relid;
    > +
    > +	/*
    > +	 * Scan the gtrs_invalidated list and add any dropped relations to the
    > +	 * gtrs_dropped list.  Since the transaction might fail later on, we need
    > +	 * the gtrs_dropped list to persist until we can successfully process it.
    > +	 */
    > +	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
    > +
    > +	/*
    > +	 * As we scan the gtrs_invalidated list, more invalidation messages might
    > +	 * arrive, so keep going until it is empty.
    > +	 */
    > +	while (gtrs_invalidated != NIL)
    > +	{
    > +		/* Pop the last relid from the list */
    > +		relid = llast_oid(gtrs_invalidated);
    >
    > Restrict relid to the while loop scope.
    >
    > 7.
    > +void
    > +TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
    > +							   ProcNumber backend, bool create)
    > +{
    > +	GtrStorageEntry *entry;
    > +	bool		found;
    > +	SMgrRelation srel;
    > +
    > +	if (create)
    > +	{
    > +		/* Initialize the storage table, if necessary */
    > +		gtr_init_storage_table();
    > +
    > +		/* Insert an entry to track the storage */
    > +		entry = hash_search(gtr_local_storage, &rlocator, HASH_ENTER, &found);
    >
    > Likewise, restrict found and srel to the if statement.
    >
    > 8.
    > +		/* Register the ON COMMIT action for relation, if it's DELETE ROWS */
    > +		Assert(relation->rd_rel->reloncommit == RELONCOMMIT_PRESERVE_ROWS ||
    > +			   relation->rd_rel->reloncommit == RELONCOMMIT_DELETE_ROWS);
    > +
    > +		if (relation->rd_rel->reloncommit == RELONCOMMIT_DELETE_ROWS)
    > +			register_on_commit_action(relation->rd_id, ONCOMMIT_DELETE_ROWS);
    >
    > How about moving the comment to just before the if statement?
    >
    > 9.
    > +void
    > +InvalidateGlobalTempRelation(Oid relid)
    > +{
    > +	MemoryContext oldcontext;
    > +	HASH_SEQ_STATUS status;
    > +	GtrUsageEntry *entry;
    > +
    > +	/* Quick exit if we haven't used any global temporary relations */
    > +	if (gtr_local_usage == NULL)
    > +		return;
    >
    > Restrict status and entry to the else statement.
    >
    > 10.
    > +void
    > +GlobalTempRelationDropped(Oid relid)
    >
    > Not a fan of this name, but no better idea yet.
    >
    > 11.
    > +void
    > +PreCommit_GlobalTempRelation(void)
    > +{
    > +	HASH_SEQ_STATUS status;
    > +	GtrStorageEntry *storage_entry;
    > +	Relation	rel;
    > +
    >
    > Restrict rel to the while loop.
    >
    > 12.
    > +List *
    > +AllGlobalTempRelationsInUse(Oid dbid)
    >
    > How about GetGlobalTempReplationsInUse()?
    >
    > 13.
    > +				ereport(ERROR,
    > +						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
    > +						 errmsg("cannot create relations in temporary schemas of other sessions")));
    >
    > For consistency with the above, I suggest changing the error message to:
    >
    >     cannot create global temporary relation in temporary schema of other sessions
    >
    > 14.
    > +         * If they're global temp relations, reassign rel2's storage to rel1.
    >
    > We use "global temporary relations" elsewhere – should we keep the same
    > terminology here?
    >
    > 15.
    > +                     errmsg(RELATION_IS_GLOBAL_TEMP(relation)
    > +                            ? "cannot create a local temporary relation as partition of global temporary relation \"%s\""
    > +                            : "cannot create a temporary relation as partition of permanent relation \"%s\"",
    >
    > Should we use the following error message:
    >
    >     cannot create a local temporary relation as partition of permanent relation \"%s\"
    >
    > 16.
    > +                            : relpersistence == RELPERSISTENCE_GLOBAL_TEMP
    > +                            ? "cannot create a global temporary relation as partition of local temporary relation \"%s\""
    >                              : "cannot create a permanent relation as partition of temporary relation \"%s\"",
    >
    > Same as above:
    >
    >     cannot create a permanent relation as partition of local temporary relation \"%s\"
    >
    > 17.
    > +            if (RELATION_IS_GLOBAL_TEMP(OldHeap) &&
    > +                IsOtherUsingGlobalTempRelation(RelationGetRelid(OldHeap)))
    > +                ereport(ERROR,
    > +                        errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    > +                        errmsg("cannot rewrite global temporary table \"%s\" because it is being used in another session",
    > +                               RelationGetRelationName(OldHeap)));
    >
    >
    > 18.
    > +                            errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    > +                            errmsg("cannot add or alter constraints of global temporary table \"%s\" because it is being used in another session",
    > +                                   get_rel_name(tab->relid)));
    >
    > 19.
    >  			if (pkrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
    >  				ereport(ERROR,
    >  						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
    > -						 errmsg("constraints on temporary tables may reference only temporary tables")));
    > +						 errmsg("constraints on local temporary tables may reference only local temporary tables")));
    >  			if (!pkrel->rd_islocaltemp || !rel->rd_islocaltemp)
    >  				ereport(ERROR,
    >  						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
    > -						 errmsg("constraints on temporary tables must involve temporary tables of this session")));
    > +						 errmsg("constraints on local temporary tables must involve local temporary tables of this session")));
    > +			break;
    > +		case RELPERSISTENCE_GLOBAL_TEMP:
    > +			if (!RELATION_IS_GLOBAL_TEMP(pkrel))
    > +				ereport(ERROR,
    > +						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
    > +						 errmsg("constraints on global temporary tables may reference only global temporary tables")));
    >
    > s/global temporary table/global temporary relation/g
    > s/local temporary table/local temporary relation/g
    >
    > 20.
    >  		attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
    >  		ereport(ERROR,
    >  				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
    > -				 errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"",
    > +				 errmsg(RELATION_IS_GLOBAL_TEMP(rel)
    > +						? "cannot attach a local temporary relation as partition of global temporary relation \"%s\""
    > +						: "cannot attach a temporary relation as partition of permanent relation \"%s\"",
    >  						RelationGetRelationName(rel))));
    >
    > Same as 15:
    >
    >     cannot attach a local temporary relation as partition of permanent relation \"%s\"
    >
    > 21.
    >  		attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP)
    >  		ereport(ERROR,
    >  				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
    > -				 errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"",
    > +				 errmsg(RELATION_IS_GLOBAL_TEMP(attachrel)
    > +						? "cannot attach a global temporary relation as partition of local temporary relation \"%s\""
    > +						: "cannot attach a permanent relation as partition of temporary relation \"%s\"",
    >  						RelationGetRelationName(rel))));
    >
    > Same as 16:
    >
    >     cannot attach a permanent relation as partition of local temporary relation \"%s\
    >
    > 22.
    > @@ -22776,7 +22828,9 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
    >  		newPartName->relpersistence == RELPERSISTENCE_TEMP)
    >  		ereport(ERROR,
    >  				errcode(ERRCODE_WRONG_OBJECT_TYPE),
    > -				errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"",
    > +				errmsg(parent_relform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP
    > +					   ? "cannot create a local temporary relation as partition of global temporary relation \"%s\""
    > +					   : "cannot create a temporary relation as partition of permanent relation \"%s\"",
    >  					   RelationGetRelationName(parent_rel)));
    >  
    >  	/* Permanent rels cannot be partitions belonging to a temporary parent. */
    >
    > Suggestion:
    >
    >     cannot create a local temporary relation as partition of permanent relation \"%s\"
    >
    > 23.
    > @@ -22784,7 +22838,9 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
    >  		parent_relform->relpersistence == RELPERSISTENCE_TEMP)
    >  		ereport(ERROR,
    >  				errcode(ERRCODE_WRONG_OBJECT_TYPE),
    > -				errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
    > +				errmsg(newPartName->relpersistence == RELPERSISTENCE_GLOBAL_TEMP
    > +					   ? "cannot create a global temporary relation as partition of local temporary relation \"%s\""
    > +					   : "cannot create a permanent relation as partition of temporary relation \"%s\"",
    >  					   RelationGetRelationName(parent_rel)));
    >
    > Suggest:
    >
    >     cannot create a permanent relation as partition of local temporary relation \"%s\"
    >
    >
    
    While testing the v5 patch, I noticed that the index of a table with ON COMMIT
    DELETE ROWS has the PRESERVE ROWS flag.  See the example below.
    
    
    	postgres=# CREATE EXTENSION pageinspect;
    	CREATE EXTENSION
    	postgres=# CREATE GLOBAL TEMPORARY TABLE gtt_delete (id int primary key, info text) ON COMMIT DELETE ROWS;
    	CREATE TABLE
    	postgres=# CREATE GLOBAL TEMPORARY TABLE gtt_preserve (id int primary key, info text);
    	CREATE TABLE
    	postgres=# SELECT relname, reloncommit FROM pg_class WHERE relname ~ '^gtt_.*';
    	      relname      | reloncommit
    	-------------------+-------------
    	 gtt_delete        | d
    	 gtt_delete_pkey   | p                <-- See here.
    	 gtt_preserve      | p
    	 gtt_preserve_pkey | p
    	(4 rows)
    	postgres=# BEGIN;
    	BEGIN
    	postgres=*# INSERT INTO gtt_delete SELECT id FROM generate_series(1, 10000) id;
    	INSERT 0 10000
    	postgres=*# SELECT * FROM bt_metap('gtt_delete_pkey');
    	 magic  | version | root | level | fastroot | fastlevel | last_cleanup_num_delpages | last_cleanup_num_tuples | allequalimage
    	--------+---------+------+-------+----------+-----------+---------------------------+-------------------------+---------------
    	 340322 |       4 |    3 |     1 |        3 |         1 |                         0 |                      -1 | t
    	(1 row)
    	
    	postgres=*# SELECT * FROM bt_page_items('gtt_delete_pkey', 1) limit 2;
    	 itemoffset | ctid  | itemlen | nulls | vars |          data           | dead | htid  | tids
    	------------+-------+---------+-------+------+-------------------------+------+-------+------
    	          1 | (1,1) |      16 | f     | f    | 6f 01 00 00 00 00 00 00 |      |       |
    	          2 | (0,1) |      16 | f     | f    | 01 00 00 00 00 00 00 00 | f    | (0,1) |
    	(2 rows)
    	
    	postgres=*# END;
    	COMMIT
    	postgres=# SELECT * FROM bt_metap('gtt_delete_pkey');
    	 magic  | version | root | level | fastroot | fastlevel | last_cleanup_num_delpages | last_cleanup_num_tuples | allequalimage
    	--------+---------+------+-------+----------+-----------+---------------------------+-------------------------+---------------
    	 340322 |       4 |    0 |     0 |        0 |         0 |                         0 |                      -1 | t
    	(1 row)
    	
    	postgres=# SELECT * FROM bt_page_items('gtt_delete_pkey', 1) limit 2;
    	ERROR:  block number 1 is out of range
    	postgres=# BEGIN;
    	BEGIN
    	postgres=*# INSERT INTO gtt_preserve SELECT id FROM generate_series(1, 10000) id;
    	INSERT 0 10000
    	postgres=*# SELECT * FROM bt_metap('gtt_preserve_pkey');
    	 magic  | version | root | level | fastroot | fastlevel | last_cleanup_num_delpages | last_cleanup_num_tuples | allequalimage
    	--------+---------+------+-------+----------+-----------+---------------------------+-------------------------+---------------
    	 340322 |       4 |    3 |     1 |        3 |         1 |                         0 |                      -1 | t
    	(1 row)
    	
    	postgres=*# SELECT * FROM bt_page_items('gtt_preserve_pkey', 1) limit 2;
    	 itemoffset | ctid  | itemlen | nulls | vars |          data           | dead | htid  | tids
    	------------+-------+---------+-------+------+-------------------------+------+-------+------
    	          1 | (1,1) |      16 | f     | f    | 6f 01 00 00 00 00 00 00 |      |       |
    	          2 | (0,1) |      16 | f     | f    | 01 00 00 00 00 00 00 00 | f    | (0,1) |
    	(2 rows)
    	
    	postgres=*# COMMIT;
    	COMMIT
    	postgres=# SELECT * FROM bt_metap('gtt_preserve_pkey');
    	 magic  | version | root | level | fastroot | fastlevel | last_cleanup_num_delpages | last_cleanup_num_tuples | allequalimage
    	--------+---------+------+-------+----------+-----------+---------------------------+-------------------------+---------------
    	 340322 |       4 |    3 |     1 |        3 |         1 |                         0 |                      -1 | t
    	(1 row)
    	
    	postgres=# SELECT * FROM bt_page_items('gtt_preserve_pkey', 1) limit 2;
    	 itemoffset | ctid  | itemlen | nulls | vars |          data           | dead | htid  | tids
    	------------+-------+---------+-------+------+-------------------------+------+-------+------
    	          1 | (1,1) |      16 | f     | f    | 6f 01 00 00 00 00 00 00 |      |       |
    	          2 | (0,1) |      16 | f     | f    | 01 00 00 00 00 00 00 00 | f    | (0,1) |
    	(2 rows)
    
    
    Is this expected behavior?
    
    Since the index data for gtt_delete is cleared upon transaction commit, I would
    expect its reloncommit flag to be 'd'.
    
    -- 
    Regards,
    Japin Li
    ChengDu WenWu Information Technology Co., Ltd.
    
    
    
    
  22. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-07-03T23:32:42Z

    On Thu, 2 Jul 2026 at 00:20, Nico Williams <nico@cryptonector.com> wrote:
    >
    > I drove Claude through a review.  You can too, so there's nothing
    > special to it, but I'll go over it here, with a bit of my content added,
    > and hopefully y'all don't hate me for sharing LLM outputs.
    
    Wow, we really will be out of a job in a few years :-)
    
    Thanks for doing that!
    
    It managed to find a lot that I'd overlooked. I'm attaching v6, with
    fixes for most of those issues plus a couple more.
    
    > 1. You have an uninitialized have_gtrs in
    >    src/backend/commands/vacuum.c:504.
    
    Fixed in v6
    
    > 2. TRUNCATE on a GTT can cause corruption.
    >
    >    Claude's description (I don't know how to evaluate this one myself):
    >
    >    RelationSetNewRelfilenumber writes session-local freeze xids into
    >    shared pg_class for GTTs (relcache.c:4057-4076). Only relfilenode is
    >    split via SetEffective_relfilenode. relpages=0, reltuples=-1,
    >    relallvisible=0, relallfrozen=0, relfrozenxid=freezeXid,
    >    relminmxid=minmulti, relpersistence are written directly into
    >    classform and committed via CatalogTupleUpdate(pg_class, ...).
    
    Yes, it needed to use the SetEffective_* methods for all those.
    
    >    Fix: the post-SetEffective_relfilenode block must update
    >    temp_classform instead of classform when temp_tuple is valid (or skip
    >    stats/freeze fields on classform entirely for GTT).
    
    v6 adds new SetEffective_relfrozenxid/relminmxid() methods and uses those.
    
    > 3. vac_get_min_tempfrozenxids() scans ProcGlobal->allProcCount without
    >    ProcArrayLock (vacuum.c:1769-1799).  Every comparable consumer
    >    (vac_update_datfrozenxid, GetSnapshotData) acquires it.
    
    Fixed in v6.
    
    > 4. eoxact_usage_list_len / eoxact_usage_list_overflowed never reset at
    >    end-of-xact (global_temp.c:1176-1179). Only the storage list counters
    >    reset.  Once a long-lived session overflows (>32 GTTs touched
    >    cumulatively), every subsequent xact falls back to full hash_seq
    >    scans.  Correctness preserved, performance silently degrades.
    
    Fixed in v6
    
    > 5. MyProc->tempfrozenxid never decays. Set on first GTT access; only
    >    moves forward on a session's VACUUM of its own GTT. A backend that
    >    touches a GTT once and never vacuums holds back pg_class.relfrozenxid
    >    advancement for all sessions for the connection's lifetime. No path
    >    clears it on GlobalTempRelationDropped or when local usage_count
    >    drops to zero.
    >
    >    I explored this one a bit because I was surprised that the frozenxid
    >    in the catalog should be relevant to GTTs.  If I understand correctly
    >    this tempfrozenxid exists because while the GTT storage is
    >    per-session, there is some coupling via the CLOG, but I don't
    >    understand why _that_ has to be, and this is what Claude told me:
    >
    >      An alternative that would have avoided the coordination
    >
    >      You could have designed it so that GTT pg_class rows are skipped
    >      from datfrozenxid computation (just like local temp), and each
    >      backend's tempfrozenxid fed directly into vac_update_datfrozenxid
    >      as an additional constraint. That would let the shared
    >      pg_class.relfrozenxid be advisory/stale without a safety issue,
    >      removing one leg of the coordination.
    >
    >      The current design chose to keep the shared row accurate — probably
    >      because it's user-visible (SELECT relfrozenxid FROM pg_class) and
    >      drives per-relation autovacuum urgency signals. That's the
    >      tradeoff: an accurate shared row, in exchange for needing
    >      cross-session horizon exchange. Same fundamental need as local
    >      temp; different plumbing because the pg_class row is shared.
    >
    >    And, ah, well, I guess you could say that `relfrozenxid` in
    >    `pg_class` is not meaningful for GTTs, or that you'll get your
    >    session's GTT's relfrozenxid.
    
    Yes, that's a very accurate description of the design choice I made.
    Having thought more about it, I think the other choice is better,
    because it's simpler, and it reduces churn on pg_class.
    
    So v6 now sets pg_class.relfrozenxid and pg_class.relminmxid to 0 for
    all global temporary relations, and only the pg_temp_class values are
    ever updated. That seems more logical, because a GTT has no storage
    until it is used by some session, so it makes sense for the frozen
    XIDs in pg_class to be 0.
    
    Also, DROP and TRUNCATE now cause tempfrozenxid and tempminmxid to be
    updated. There was another bug there -- things like DROP, REPACK, and
    TRUNCATE that advance tempfrozenxid and tempminmxid should only do so
    if the transaction commits. Each of those (but not VACUUM) can be
    rolled back, in which case tempfrozenxid and tempminmxid should not be
    advanced.
    
    > 6. InsertPgTempIndexTuple mutates rel->rd_index->indisvalid in-place
    >    (global_temp.c:885-892).  Relcache fields are normally immutable
    >    outside relcache.c.  Works today but fragile.
    
    Actually, it's InitGlobalTempRelation() that does this, not
    InsertPgTempIndexTuple(). InitGlobalTempRelation() is only called from
    one function in relcache.c, as the final part of setting up the
    relcache entry. In an early prototype, I had InitGlobalTempRelation()
    in relcache.c, and I guess it could be moved back, but I don't think
    it's particularly bad the way it is.
    
    > 7. AbortTransaction ordering — gtrs_dropped is list_freed
    >    unconditionally in AtEOXact (:1178) even though the in-source comment
    >    (:1077-1080) claims it'll be "repeated next commit". Mostly benign
    >    (storage is reaped at exit) but the comment is wrong.
    
    Yeah, when I originally started writing that, I intended that it would
    be repeated on the next commit, but in the end, I decided maybe that
    wouldn't be a good idea, because if it failed for some reason, it
    might keep failing, making every subsequent transaction fail. So I've
    just updated that comment, and left that code as it was.
    
    > 8. DeleteRelationTuple calls GlobalTempRelationDropped after
    >    CatalogTupleDelete; GlobalTempRelationDropped then calls
    >    get_rel_relkind(relid) (global_temp.c:1038). After a CCI, the
    >    syscache will return '\0' and skip DeletePgTempIndexTuple. Pass
    >    relkind explicitly instead of re-deriving it.
    
    This was a nice catch. I fixed it in v6 by saving the relkind on the
    usage entry, so it doesn't need to look it up again.
    
    In addition to those, I also fixed a couple of things in ON COMMIT
    DELETE ROWS processing:
    
    1. PreCommit_GlobalTempRelation() needs to be called *before* ON
    COMMIT handling, in case another session drops a GTT with ON COMMIT
    DELETE ROWS -- we need to remove it from the on_commits list before ON
    COMMIT processing happens.
    
    2. The ON COMMIT DELETE ROWS processing now does its work with a
    RowExclusiveLock for GTTs, preventing blocking/deadlock risks.
    
    I'll try and post another update soon, addressing other people's
    review comments.
    
    Regards,
    Dean
    
  23. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-07-03T23:37:12Z

    On Wed, 1 Jul 2026 at 07:24, Japin Li <japinli@hotmail.com> wrote:
    >
    > While testing the v5 patch, I encountered a lock wait.
    >
    > 2026-07-01 14:18:43.603 CST [1486593] LOG:  process 1486593 still waiting for AccessExclusiveLock on relation 16384 of database 5 after 1000.106 ms
    > 2026-07-01 14:18:43.603 CST [1486593] DETAIL:  Process holding the lock: 1486484. Wait queue: 1486593.
    > 2026-07-01 14:18:43.603 CST [1486593] CONTEXT:  waiting for AccessExclusiveLock on relation 16384 of database 5
    > 2026-07-01 14:18:43.603 CST [1486593] STATEMENT:  SELECT * FROM gtt_delete;
    >
    > To reproduce:
    >
    > Session 1:
    >
    > CREATE GLOBAL TEMPORARY TABLE gtt_delete (id int) ON COMMIT DELETE ROWS;
    > BEGIN;
    > INSERT INTO gtt_delete VALUES (1);
    >
    > Session 2:
    >
    > SELECT * FROM gtt_delete;   ---> blocked
    >
    > Is this expected?
    
    Thanks for testing!
    
    This is fixed in v6.
    
    Regards,
    Dean
    
    
    
    
  24. Re: Global temporary tables

    Nico Williams <nico@cryptonector.com> — 2026-07-04T01:00:49Z

    On Sat, Jul 04, 2026 at 12:32:42AM +0100, Dean Rasheed wrote:
    > On Thu, 2 Jul 2026 at 00:20, Nico Williams <nico@cryptonector.com> wrote:
    > >
    > > I drove Claude through a review.  You can too, so there's nothing
    > > special to it, but I'll go over it here, with a bit of my content added,
    > > and hopefully y'all don't hate me for sharing LLM outputs.
    > 
    > Wow, we really will be out of a job in a few years :-)
    
    I seriously doubt it.  And I'm guessing you coded w/o assistance, so
    hats off because it looks pretty good.
    
    I only used an LLM for the review because I'm not sufficiently familiar
    with PG internals to do a solid review myself, and I want to help this
    along.
    
    > >    And, ah, well, I guess you could say that `relfrozenxid` in
    > >    `pg_class` is not meaningful for GTTs, or that you'll get your
    > >    session's GTT's relfrozenxid.
    > 
    > Yes, that's a very accurate description of the design choice I made.
    > Having thought more about it, I think the other choice is better,
    > because it's simpler, and it reduces churn on pg_class.
    
    Good.  I think seeing some other session's frozenxid would be weird, and
    a bit of a leak.
    
    > So v6 now sets pg_class.relfrozenxid and pg_class.relminmxid to 0 for
    > all global temporary relations, and only the pg_temp_class values are
    > ever updated. That seems more logical, because a GTT has no storage
    > until it is used by some session, so it makes sense for the frozen
    > XIDs in pg_class to be 0.
    
    Yes.
    
    > I'll try and post another update soon, addressing other people's
    > review comments.
    
    I'll drive an LLM through this too sometime, especially with an eye to
    testing.
    
    I'm super excited to have GTTs in PG.  It's going to make all sorts of
    things much easier for me and many many others, so thank you so much!
    
    Nico
    -- 
    
    
    
    
  25. Re: Global temporary tables

    John Naylor <johncnaylorls@gmail.com> — 2026-07-05T08:16:48Z

    On Sat, Jul 4, 2026 at 6:33 AM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    > So v6 now sets pg_class.relfrozenxid and pg_class.relminmxid to 0 for
    > all global temporary relations, and only the pg_temp_class values are
    > ever updated. That seems more logical, because a GTT has no storage
    > until it is used by some session, so it makes sense for the frozen
    > XIDs in pg_class to be 0.
    
    Unfortunately, the cure seems to be worse than the disease:
    
    -- Session 1
    CREATE GLOBAL TEMPORARY TABLE g (a int);
    -- Session 1 is no longer needed.
    
    -- Session 2 (a fresh session that has never touched g)
    
    BEGIN;
      SELECT count(*) FROM g;    -- first access here: queues g's pg_temp_class row
                                 -- (still unflushed inside this transaction)
      SAVEPOINT s1;
        DROP TABLE g;            -- removes that pending queue entry
      ROLLBACK TO s1;            -- pg_class row is restored by MVCC;
                                 -- the queue entry is not restored
      INSERT INTO g VALUES (1);  -- the table still works normally
    COMMIT;
    
    -- The table is live and holds committed data:
    SELECT count(*) AS live_rows FROM g;
    --  live_rows
    -- -----------
    --          1
    
    -- The table cannot be vacuumed.
    -- On assert builds it hits an assert. On production builds...
    VACUUM g;
    -- WARNING:  bypassing nonessential maintenance of table
    "john.public.g" as a failsafe after 0 index scans
    -- DETAIL:  The table's relfrozenxid or relminmxid is too far in the past.
    -- HINT:  Consider increasing configuration parameter
    "maintenance_work_mem" or "autovacuum_work_mem".
    -- You might also need to consider other ways for VACUUM to keep up
    with the allocation of transaction IDs.
    -- ERROR:  cache lookup failed for global temp relation 16385
    
    -- The error is because there is no matching row in pg_temp_class
    SELECT count(*) AS temp_class_rows
    FROM   pg_temp_class
    WHERE  oid = 'g'::regclass;
    --  temp_class_rows
    -- -----------------
    --                0
    
    -- ...so dropping doesn't work either:
    DROP TABLE g;
    -- ERROR:  cache lookup failed for global temp relation 16385
    
    I'm starting to think the queue mechanism needs serious attention, if
    not a complete re-think.
    
    
    On Mon, Jun 22, 2026 at 8:43 PM Andrew Dunstan <andrew@dunslane.net> wrote:
    > > On Jun 22, 2026, at 4:57 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    > > Oh, wow. There's a lot of similarity between our patchsets, which is
    > > reassuring, but there are also a number of differences, which I need
    > > to think about in more detail.
    >
    > Yeah, agree on both fronts. The main areas I see are the catalog and handling wraparound. I don’t think there’s necessarily a clear winner on either, so I’ll be interested to hear what others say.
    
    I was actually hoping to hear more about how the approaches differ so
    we can weigh the architectural tradeoffs from both operational and
    maintenance standpoints. (I suppose I just volunteered myself for
    that, didn't I...)
    
    --
    John Naylor
    Amazon Web Services
    
    
    
    
  26. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-07-05T09:58:44Z

    On Sun, 5 Jul 2026 at 09:17, John Naylor <johncnaylorls@gmail.com> wrote:
    >
    > BEGIN;
    >   SELECT count(*) FROM g;    -- first access here: queues g's pg_temp_class row
    >                              -- (still unflushed inside this transaction)
    >   SAVEPOINT s1;
    >     DROP TABLE g;            -- removes that pending queue entry
    >   ROLLBACK TO s1;            -- pg_class row is restored by MVCC;
    >                              -- the queue entry is not restored
    >   INSERT INTO g VALUES (1);  -- the table still works normally
    > COMMIT;
    >
    > I'm starting to think the queue mechanism needs serious attention, if
    > not a complete re-think.
    
    Thanks for looking!
    
    Yes, you're right. In that example, the pg_temp_class tuple gets lost
    entirely. It really needs the same sort of sub XID tracking as the
    usage/storage code, to ensure that they never get lost. I'll take a
    look.
    
    Regards,
    Dean
    
    
    
    
  27. Re: Global temporary tables

    Japin Li <japinli@hotmail.com> — 2026-07-06T08:35:06Z

    Hi, Dean
    
    Thanks for updating the v6 patch.
    
    On Sat, 04 Jul 2026 at 00:37, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    > On Wed, 1 Jul 2026 at 07:24, Japin Li <japinli@hotmail.com> wrote:
    >>
    >> While testing the v5 patch, I encountered a lock wait.
    >>
    >> 2026-07-01 14:18:43.603 CST [1486593] LOG:  process 1486593 still waiting for AccessExclusiveLock on relation 16384 of database 5 after 1000.106 ms
    >> 2026-07-01 14:18:43.603 CST [1486593] DETAIL:  Process holding the lock: 1486484. Wait queue: 1486593.
    >> 2026-07-01 14:18:43.603 CST [1486593] CONTEXT:  waiting for AccessExclusiveLock on relation 16384 of database 5
    >> 2026-07-01 14:18:43.603 CST [1486593] STATEMENT:  SELECT * FROM gtt_delete;
    >>
    >> To reproduce:
    >>
    >> Session 1:
    >>
    >> CREATE GLOBAL TEMPORARY TABLE gtt_delete (id int) ON COMMIT DELETE ROWS;
    >> BEGIN;
    >> INSERT INTO gtt_delete VALUES (1);
    >>
    >> Session 2:
    >>
    >> SELECT * FROM gtt_delete;   ---> blocked
    >>
    >> Is this expected?
    >
    > Thanks for testing!
    >
    > This is fixed in v6.
    >
    
    I tested the v6 patch, and the blocked is fixed.
    
    During testing of the v6 patch, I observed that sequences belonging to global
    temporary tables defined with ON COMMIT DELETE ROWS are not reset after the
    transaction commits.
    
    This is similar to the index I found in [1].  Here is the example:
    
        INSERT INTO gtt_delete (info) VALUES ('row 1');
        postgres=# CREATE GLOBAL TEMPORARY TABLE gtt_delete (id serial primary key, info text) ON COMMIT DELETE ROWS;
        CREATE TABLE
        postgres=# SELECT relname, reloncommit FROM pg_class WHERE relname ~ '^gtt_delete.*';
              relname      | reloncommit
        -------------------+-------------
         gtt_delete        | d
         gtt_delete_id_seq | p      <-- The sequence is marked as PRESERVE.
         gtt_delete_pkey   | p      
        (3 rows)
        
        postgres=# SELECT currval('gtt_delete_id_seq');
        ERROR:  currval of sequence "gtt_delete_id_seq" is not yet defined in this session
        postgres=# BEGIN;
        BEGIN
        postgres=*# INSERT INTO gtt_delete (info) VALUES ('row 1');
        INSERT 0 1
        postgres=*# SELECT * FROM gtt_delete;
         id | info
        ----+-------
          1 | row 1
        (1 row)
        
        postgres=*# SELECT currval('gtt_delete_id_seq');
         currval
        ---------
               1
        (1 row)
        
        postgres=*# END;
        COMMIT
        postgres=# SELECT currval('gtt_delete_id_seq');  -- The sequence still exists.
         currval
        ---------
               1
        (1 row)
        postgres=*# END;
        COMMIT
        postgres=# SELECT currval('gtt_delete_id_seq');  -- The sequence doesn't reset.
         currval
        ---------
               2
        (1 row)
    
    The sequence doesn't reset after commit – is this intended?
    
    [1] https://postgr.es/m/SY7PR01MB1092159B3ED3CA6F4401E465EB6F42@SY7PR01MB10921.ausprd01.prod.outlook.com
    
    > Regards,
    > Dean
    
    -- 
    Regards,
    Japin Li
    ChengDu WenWu Information Technology Co., Ltd.
    
    
    
    
  28. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-07-06T09:43:42Z

    On Sun, 5 Jul 2026 at 10:58, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    >
    > On Sun, 5 Jul 2026 at 09:17, John Naylor <johncnaylorls@gmail.com> wrote:
    > >
    > > I'm starting to think the queue mechanism needs serious attention, if
    > > not a complete re-think.
    >
    > Yes, you're right. In that example, the pg_temp_class tuple gets lost
    > entirely. It really needs the same sort of sub XID tracking as the
    > usage/storage code
    
    Attached is v7, doing that.
    
    Locally, I have been stress-testing this using a hacked-together
    python script that does random sequences of CREATE, DROP, BEGIN,
    SAVEPOINT, RELEASE, ROLLBACK TO, INSERT, SELECT, COMMIT, ROLLBACK, and
    TRUNCATE. I'm somewhat disappointed that it didn't pick up the
    original problem, so I'll go back and look at it again.
    
    v7 now also includes tempfrozenxid and tempminmxid in the
    pg_stat_activity view, allowing those to be monitored.
    
    Regards,
    Dean
    
  29. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-07-06T10:12:32Z

    On Mon, 6 Jul 2026 at 09:35, Japin Li <japinli@hotmail.com> wrote:
    >
    > During testing of the v6 patch, I observed that sequences belonging to global
    > temporary tables defined with ON COMMIT DELETE ROWS are not reset after the
    > transaction commits.
    >
    >     postgres=# BEGIN;
    >     postgres=*# INSERT INTO gtt_delete (info) VALUES ('row 1');
    >     postgres=*# SELECT * FROM gtt_delete;
    >     postgres=*# SELECT currval('gtt_delete_id_seq');
    >     postgres=*# END;
    >     postgres=# SELECT currval('gtt_delete_id_seq');  -- The sequence still exists.
    >      currval
    >     ---------
    >            1
    >     (1 row)
    >     postgres=*# END;
    >     COMMIT
    >     postgres=# SELECT currval('gtt_delete_id_seq');  -- The sequence doesn't reset.
    >      currval
    >     ---------
    >            2
    >     (1 row)
    >
    > The sequence doesn't reset after commit – is this intended?
    >
    
    I think that's what I would expect. This matches what happens with
    normal (local) temp tables -- sequences are not reset after commit (or
    rollback, for that matter).
    
    Regarding pg_class.reloncommit, 'p' (preserve) seems correct for a
    sequence attached to a GTT with ON COMMIT DELETE ROWS, because the
    sequence data is preserved. For indexes, not so much, because the
    index data is deleted on commit. I guess it could be NULL for anything
    that's not a table.
    
    Andrew's patch uses a reloption to store the table's on-commit state,
    but to me that doesn't seem quite right, and it requires special code
    to mark it as a "private" reloption that is non-user-editable. It does
    simplify the question of what to do for non-tables though. What do
    others think?
    
    Regards,
    Dean
    
    
    
    
  30. Re: Global temporary tables

    Zsolt Parragi <zsolt.parragi@percona.com> — 2026-07-06T21:53:51Z

    Hello!
    
    This is a great feature, I did some testing of it.
    
    1. If a table has an ON COMMIT clause:
    
    CREATE GLOBAL TEMPORARY TABLE gtt_del (id int) ON COMMIT DELETE ROWS
    
    Then pg_dump forgets that.
    
    2. Even an aborted transaction pins frozenxid:
    
    CREATE GLOBAL TEMPORARY TABLE gtt (id int);
    
    In one session
    
    BEGIN; SELECT count(*) FROM gtt; ABORT;
    -- and keep session alive
    
    Then in another session,
    SELECT datfrozenxid FROM pg_database WHERE datname=current_database();
    -- do some work
    VACUUM (FREEZE);
    SELECT datfrozenxid FROM pg_database WHERE datname=current_database();
    -- ...
    
    And repeat the above a few times for the second session, datfrozenxid
    will remain the same until the first session is closed.
    
    3. in AtEOSubXact_UsageCleanup and also in AtEOSubXact_StorageCleanup
    
    +		else
    +			gtr_remove_usage(entry->relid);
    +	}
    +
    +	/* Update the usage stopped subid */
    +	if (entry->stopped_subid == mySubid)
    +	{
    +		if (isCommit)
    +			entry->stopped_subid = parentSubid;
    +		else
    +			entry->stopped_subid = InvalidSubTransactionId;
    +	}
    
    if we follow the else branch with gtr_remove_usage, shouldn't the
    function immediately return?
    
    4. in AtEOSubXact_PendingInsertCleanup
    
    +		else
    +			hash_search(pending_inserts, &entry->relid, HASH_REMOVE, NULL);
    +	}
    +
    +	/* Update the inserted subid (may rollback flush) */
    +	if (entry->inserted_subid == mySubid)
    +	{
    +		if (isCommit)
    +			entry->inserted_subid = parentSubid;
    +		else
    +		{
    +			entry->inserted_subid = InvalidSubTransactionId;
    +			have_inserts_to_flush = true;
    +		}
    +	}
    
    same question, isn't the else branch missing a return?
    
    
    5. in gtr_remove_usage / gtr_remove_all_usage_on_exit
    
    +	shared_entry = dshash_find(gtr_shared_usage, &key, true);
    +
    +	if (shared_entry->usage_count > 1)
    
    and in gtr_record_usage
    
    +	/* Flag the usage entry for eoxact cleanup */
    +	EOXactUsageListAdd(relid);
    +
    +	/* Add/update shared usage entry */
    +	key.dbid = MyDatabaseId;
    +	key.relid = relid;
    +	shared_entry = dshash_find_or_insert(gtr_shared_usage, &key, &found);
    
    Can't this crash with a null pointer access in the above if
    dshash_find_or_insert fails with OOM?
    
    
    
    
  31. Re: Global temporary tables

    Japin Li <japinli@hotmail.com> — 2026-07-07T01:09:44Z

    On Mon, 06 Jul 2026 at 11:12, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    > On Mon, 6 Jul 2026 at 09:35, Japin Li <japinli@hotmail.com> wrote:
    >>
    >> During testing of the v6 patch, I observed that sequences belonging to global
    >> temporary tables defined with ON COMMIT DELETE ROWS are not reset after the
    >> transaction commits.
    >>
    >>     postgres=# BEGIN;
    >>     postgres=*# INSERT INTO gtt_delete (info) VALUES ('row 1');
    >>     postgres=*# SELECT * FROM gtt_delete;
    >>     postgres=*# SELECT currval('gtt_delete_id_seq');
    >>     postgres=*# END;
    >>     postgres=# SELECT currval('gtt_delete_id_seq');  -- The sequence still exists.
    >>      currval
    >>     ---------
    >>            1
    >>     (1 row)
    >>     postgres=*# END;
    >>     COMMIT
    >>     postgres=# SELECT currval('gtt_delete_id_seq');  -- The sequence doesn't reset.
    >>      currval
    >>     ---------
    >>            2
    >>     (1 row)
    >>
    >> The sequence doesn't reset after commit – is this intended?
    >>
    >
    > I think that's what I would expect. This matches what happens with
    > normal (local) temp tables -- sequences are not reset after commit (or
    > rollback, for that matter).
    >
    
    Thanks for the explanation!  I hadn't noticed this before.
    
    > Regarding pg_class.reloncommit, 'p' (preserve) seems correct for a
    > sequence attached to a GTT with ON COMMIT DELETE ROWS, because the
    > sequence data is preserved. For indexes, not so much, because the
    > index data is deleted on commit. I guess it could be NULL for anything
    > that's not a table.
    >
    
    +1 for setting it to NULL for non‑table objects, since CREATE INDEX and
    CREATE SEQUENCE do not support the ON COMMIT clause.
    
    > Andrew's patch uses a reloption to store the table's on-commit state,
    > but to me that doesn't seem quite right, and it requires special code
    > to mark it as a "private" reloption that is non-user-editable. It does
    > simplify the question of what to do for non-tables though. What do
    > others think?
    >
    
    I'm on board with your approach to the on-commit state.
    
    > Regards,
    > Dean
    
    -- 
    Regards,
    Japin Li
    ChengDu WenWu Information Technology Co., Ltd.
    
    
    
    
  32. Re: Global temporary tables

    John Naylor <johncnaylorls@gmail.com> — 2026-07-07T09:06:01Z

    On Mon, Jul 6, 2026 at 4:43 PM Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    >
    > On Sun, 5 Jul 2026 at 10:58, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
    > > Yes, you're right. In that example, the pg_temp_class tuple gets lost
    > > entirely. It really needs the same sort of sub XID tracking as the
    > > usage/storage code
    >
    > Attached is v7, doing that.
    
    Okay, I followed up with various attempts to get the new sub XID
    tracking to do something dire, and couldn't find anything.
    
    I did find something else, however:
    
    set statement_timeout  = '3s';
    set debug_discard_caches = 1;
    
    create global temporary table g (a int);
    
    This enters an infinite loop after passing the point where the timeout
    would have done any good, and it looks like
    gtr_process_invalidated_gtrs() didn't take into account whole-cache
    invalidation -- here's a partial backtrace:
    
    (BTW, I forgot to mention I set this WoA in my last message -- I've
    left it like that for now)
    
    #21 0x00000000009c0d04 in RelationCacheInvalidate
    (debug_discard=debug_discard@entry=true) at
    ../src/backend/utils/cache/relcache.c:3209
    #22 0x00000000009b0c4e in InvalidateSystemCachesExtended
    (debug_discard=debug_discard@entry=true) at
    ../src/backend/utils/cache/inval.c:793
    #23 0x00000000009b0d5b in AcceptInvalidationMessages () at
    ../src/backend/utils/cache/inval.c:975
    #24 0x0000000000858f80 in LockRelationOid (relid=2662, lockmode=1) at
    ../src/backend/storage/lmgr/lmgr.c:136
    #25 0x00000000004c3dfd in relation_open
    (relationId=relationId@entry=2662, lockmode=lockmode@entry=1) at
    ../src/backend/access/common/relation.c:56
    #26 0x0000000000528579 in index_open
    (relationId=relationId@entry=2662, lockmode=lockmode@entry=1) at
    ../src/backend/access/index/indexam.c:138
    #27 0x0000000000527cf7 in systable_beginscan
    (heapRelation=heapRelation@entry=0x7f2f5efbeeb8,
    indexId=indexId@entry=2662, indexOK=indexOK@entry=true,
        snapshot=snapshot@entry=0x0, nkeys=nkeys@entry=1,
    key=key@entry=0x7ffc1b7043b0) at
    ../src/backend/access/index/genam.c:400
    #28 0x00000000009b89fe in ScanPgRelation (targetRelId=1259,
    indexOK=indexOK@entry=true,
    force_non_historic=force_non_historic@entry=false)
        at ../src/backend/utils/cache/relcache.c:397
    #29 0x00000000009c060d in RelationReloadNailed
    (relation=0x7f2f5efbeeb8) at
    ../src/backend/utils/cache/relcache.c:2540
    #30 RelationRebuildRelation (relation=0x7f2f5efbeeb8) at
    ../src/backend/utils/cache/relcache.c:2738
    #31 0x00000000009c0e76 in RelationIdGetRelation (relationId=<optimized
    out>, relationId@entry=1259) at
    ../src/backend/utils/cache/relcache.c:2197
    #32 0x00000000004c3e05 in relation_open (relationId=1259,
    lockmode=lockmode@entry=1) at
    ../src/backend/access/common/relation.c:59
    #33 0x0000000000561579 in table_open (relationId=<optimized out>,
    lockmode=lockmode@entry=1) at ../src/backend/access/table/table.c:44
    #34 0x00000000009add60 in SearchCatCacheMiss
    (cache=cache@entry=0xaedd380, nkeys=nkeys@entry=1,
    hashValue=628204292, hashIndex=4, v1=v1@entry=24576,
        v2=v2@entry=0, v3=0, v4=0) at ../src/backend/utils/cache/catcache.c:1577
    #35 0x00000000009ae039 in SearchCatCacheInternal (cache=0xaedd380,
    nkeys=1, v1=24576, v2=0, v3=0, v4=0) at
    ../src/backend/utils/cache/catcache.c:1522
    #36 0x00000000009c4159 in SearchSysCacheExists
    (cacheId=cacheId@entry=RELOID, key1=key1@entry=24576,
    key2=key2@entry=0, key3=key3@entry=0,
        key4=key4@entry=0) at ../src/backend/utils/cache/syscache.c:434
    #37 0x00000000005a9a43 in gtr_process_invalidated_gtrs () at
    ../src/backend/catalog/global_temp.c:706
    #38 PreCommit_GlobalTempRelation () at ../src/backend/catalog/global_temp.c:1071
    #39 0x0000000000577556 in CommitTransaction () at
    ../src/backend/access/transam/xact.c:2360
    
    --
    John Naylor
    Amazon Web Services
    
    
    
    
  33. Re: Global temporary tables

    Dean Rasheed <dean.a.rasheed@gmail.com> — 2026-07-07T22:37:11Z

    On Tue, 7 Jul 2026 at 10:06, John Naylor <johncnaylorls@gmail.com> wrote:
    >
    > Okay, I followed up with various attempts to get the new sub XID
    > tracking to do something dire, and couldn't find anything.
    >
    
    Unfortunately, it still wasn't working properly. In a fresh session,
    doing VACUUM FREEZE on a GTT with ON COMMIT DELETE ROWS would fail
    because the VACUUM FREEZE would update a new pg_temp_class tuple,
    flushing it to the database, and then the ON COMMIT code would fail to
    find it, because there was no intervening CommandCounterIncrement() to
    make it visible.
    
    I don't want to go round adding CommandCounterIncrement()'s to fix
    things like this -- part of the reason for having the pending inserts
    was to make that unnecessary. So instead, in v8, I have made it so
    that the pending insert entries are kept up-to-date all the way to the
    end of the transaction, even if they're flushed to the database before
    that. This turns out to make the code far simpler, and easier to
    reason about, as well as hopefully being more robust. So I feel much
    better about this formulation of pending inserts.
    
    > I did find something else, however:
    >
    > set statement_timeout  = '3s';
    > set debug_discard_caches = 1;
    >
    > create global temporary table g (a int);
    >
    > This enters an infinite loop
    >
    
    Doh! Fixed in v8.
    
    Regards,
    Dean