/* ------------------------------------------------------------------------- * * pgstat_relation.c * Implementation of relation statistics. * * This file contains the implementation of function relation. It is kept * separate from pgstat.c to enforce the line between the statistics access / * storage implementation and the details about individual types of * statistics. * * Copyright (c) 2001-2024, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_relation.c * ------------------------------------------------------------------------- */ #include "postgres.h" #include "access/twophase_rmgr.h" #include "access/xact.h" #include "catalog/catalog.h" #include "postmaster/autovacuum.h" #include "utils/memutils.h" #include "utils/pgstat_internal.h" #include "utils/rel.h" #include "utils/timestamp.h" /* Record that's written to 2PC state file when pgstat state is persisted */ typedef struct TwoPhasePgStatRecord { PgStat_Counter tuples_inserted; /* tuples inserted in xact */ PgStat_Counter tuples_updated; /* tuples updated in xact */ PgStat_Counter tuples_deleted; /* tuples deleted in xact */ /* tuples i/u/d prior to truncate/drop */ PgStat_Counter inserted_pre_truncdrop; PgStat_Counter updated_pre_truncdrop; PgStat_Counter deleted_pre_truncdrop; Oid id; /* table's OID */ bool shared; /* is it a shared catalog? */ bool truncdropped; /* was the relation truncated/dropped? */ } TwoPhasePgStatRecord; static PgStat_TableStatus *pgstat_prep_relation_pending(Oid rel_id, bool isshared); static void add_tabstat_xact_level(PgStat_TableStatus *pgstat_info, int nest_level); static void ensure_tabstat_xact_level(PgStat_TableStatus *pgstat_info); static void save_truncdrop_counters(PgStat_TableXactStatus *trans, bool is_drop); static void restore_truncdrop_counters(PgStat_TableXactStatus *trans); /* * Copy stats between relations. This is used for things like REINDEX * CONCURRENTLY. */ void pgstat_copy_relation_stats(Relation dst, Relation src) { PgStat_StatTabEntry *srcstats; PgStatShared_Relation *dstshstats; PgStat_EntryRef *dst_ref; srcstats = pgstat_fetch_stat_tabentry_ext(src->rd_rel->relisshared, RelationGetRelid(src)); if (!srcstats) return; dst_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dst->rd_rel->relisshared ? InvalidOid : MyDatabaseId, RelationGetRelid(dst), false); dstshstats = (PgStatShared_Relation *) dst_ref->shared_stats; dstshstats->stats = *srcstats; pgstat_unlock_entry(dst_ref); } /* * Initialize a relcache entry to count access statistics. Called whenever a * relation is opened. * * We assume that a relcache entry's pgstat_info field is zeroed by relcache.c * when the relcache entry is made; thereafter it is long-lived data. * * This does not create a reference to a stats entry in shared memory, nor * allocate memory for the pending stats. That happens in * pgstat_assoc_relation(). */ void pgstat_init_relation(Relation rel) { char relkind = rel->rd_rel->relkind; /* * We only count stats for relations with storage and partitioned tables */ if (!RELKIND_HAS_STORAGE(relkind) && relkind != RELKIND_PARTITIONED_TABLE) { rel->pgstat_enabled = false; rel->pgstat_info = NULL; return; } if (!pgstat_track_counts) { if (rel->pgstat_info) pgstat_unlink_relation(rel); /* We're not counting at all */ rel->pgstat_enabled = false; rel->pgstat_info = NULL; return; } rel->pgstat_enabled = true; } /* * Prepare for statistics for this relation to be collected. * * This ensures we have a reference to the stats entry before stats can be * generated. That is important because a relation drop in another connection * could otherwise lead to the stats entry being dropped, which then later * would get recreated when flushing stats. * * This is separate from pgstat_init_relation() as it is not uncommon for * relcache entries to be opened without ever getting stats reported. */ void pgstat_assoc_relation(Relation rel) { Assert(rel->pgstat_enabled); Assert(rel->pgstat_info == NULL); /* Else find or make the PgStat_TableStatus entry, and update link */ rel->pgstat_info = pgstat_prep_relation_pending(RelationGetRelid(rel), rel->rd_rel->relisshared); /* don't allow link a stats to multiple relcache entries */ Assert(rel->pgstat_info->relation == NULL); /* mark this relation as the owner */ rel->pgstat_info->relation = rel; } /* * Break the mutual link between a relcache entry and pending stats entry. * This must be called whenever one end of the link is removed. */ void pgstat_unlink_relation(Relation rel) { /* remove the link to stats info if any */ if (rel->pgstat_info == NULL) return; /* link sanity check */ Assert(rel->pgstat_info->relation == rel); rel->pgstat_info->relation = NULL; rel->pgstat_info = NULL; } /* * Ensure that stats are dropped if transaction aborts. */ void pgstat_create_relation(Relation rel) { pgstat_create_transactional(PGSTAT_KIND_RELATION, rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId, RelationGetRelid(rel)); } /* * Ensure that stats are dropped if transaction commits. */ void pgstat_drop_relation(Relation rel) { int nest_level = GetCurrentTransactionNestLevel(); PgStat_TableStatus *pgstat_info; pgstat_drop_transactional(PGSTAT_KIND_RELATION, rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId, RelationGetRelid(rel)); if (!pgstat_should_count_relation(rel)) return; /* * Transactionally set counters to 0. That ensures that accesses to * pg_stat_xact_all_tables inside the transaction show 0. */ pgstat_info = rel->pgstat_info; if (pgstat_info->trans && pgstat_info->trans->nest_level == nest_level) { save_truncdrop_counters(pgstat_info->trans, true); pgstat_info->trans->tuples_inserted = 0; pgstat_info->trans->tuples_updated = 0; pgstat_info->trans->tuples_deleted = 0; } } /* * Report that the table was just vacuumed and flush IO statistics. */ void pgstat_report_vacuum(Oid tableoid, bool shared, PgStat_Counter livetuples, PgStat_Counter deadtuples) { PgStat_EntryRef *entry_ref; PgStatShared_Relation *shtabentry; PgStat_StatTabEntry *tabentry; Oid dboid = (shared ? InvalidOid : MyDatabaseId); TimestampTz ts; if (!pgstat_track_counts) return; /* Store the data in the table's hash table entry. */ ts = GetCurrentTimestamp(); /* block acquiring lock for the same reason as pgstat_report_autovac() */ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid, tableoid, false); shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats; tabentry = &shtabentry->stats; tabentry->live_tuples = livetuples; tabentry->dead_tuples = deadtuples; /* * It is quite possible that a non-aggressive VACUUM ended up skipping * various pages, however, we'll zero the insert counter here regardless. * It's currently used only to track when we need to perform an "insert" * autovacuum, which are mainly intended to freeze newly inserted tuples. * Zeroing this may just mean we'll not try to vacuum the table again * until enough tuples have been inserted to trigger another insert * autovacuum. An anti-wraparound autovacuum will catch any persistent * stragglers. */ tabentry->ins_since_vacuum = 0; if (AmAutoVacuumWorkerProcess()) { tabentry->last_autovacuum_time = ts; tabentry->autovacuum_count++; } else { tabentry->last_vacuum_time = ts; tabentry->vacuum_count++; } pgstat_unlock_entry(entry_ref); /* * Flush IO statistics now. pgstat_report_stat() will flush IO stats, * however this will not be called until after an entire autovacuum cycle * is done -- which will likely vacuum many relations -- or until the * VACUUM command has processed all tables and committed. */ pgstat_flush_io(false); } /* * Report that the table was just analyzed and flush IO statistics. * * Caller must provide new live- and dead-tuples estimates, as well as a * flag indicating whether to reset the mod_since_analyze counter. */ void pgstat_report_analyze(Relation rel, PgStat_Counter livetuples, PgStat_Counter deadtuples, bool resetcounter) { PgStat_EntryRef *entry_ref; PgStatShared_Relation *shtabentry; PgStat_StatTabEntry *tabentry; Oid dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId); if (!pgstat_track_counts) return; /* * Unlike VACUUM, ANALYZE might be running inside a transaction that has * already inserted and/or deleted rows in the target table. ANALYZE will * have counted such rows as live or dead respectively. Because we will * report our counts of such rows at transaction end, we should subtract * off these counts from the update we're making now, else they'll be * double-counted after commit. (This approach also ensures that the * shared stats entry ends up with the right numbers if we abort instead * of committing.) * * Waste no time on partitioned tables, though. */ if (pgstat_should_count_relation(rel) && rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) { PgStat_TableXactStatus *trans; for (trans = rel->pgstat_info->trans; trans; trans = trans->upper) { livetuples -= trans->tuples_inserted - trans->tuples_deleted; deadtuples -= trans->tuples_updated + trans->tuples_deleted; } /* count stuff inserted by already-aborted subxacts, too */ deadtuples -= rel->pgstat_info->counts.delta_dead_tuples; /* Since ANALYZE's counts are estimates, we could have underflowed */ livetuples = Max(livetuples, 0); deadtuples = Max(deadtuples, 0); } /* block acquiring lock for the same reason as pgstat_report_autovac() */ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid, RelationGetRelid(rel), false); /* can't get dropped while accessed */ Assert(entry_ref != NULL && entry_ref->shared_stats != NULL); shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats; tabentry = &shtabentry->stats; tabentry->live_tuples = livetuples; tabentry->dead_tuples = deadtuples; /* * If commanded, reset mod_since_analyze to zero. This forgets any * changes that were committed while the ANALYZE was in progress, but we * have no good way to estimate how many of those there were. */ if (resetcounter) tabentry->mod_since_analyze = 0; if (AmAutoVacuumWorkerProcess()) { tabentry->last_autoanalyze_time = GetCurrentTimestamp(); tabentry->autoanalyze_count++; } else { tabentry->last_analyze_time = GetCurrentTimestamp(); tabentry->analyze_count++; } pgstat_unlock_entry(entry_ref); /* see pgstat_report_vacuum() */ pgstat_flush_io(false); } /* * count a tuple insertion of n tuples */ void pgstat_count_heap_insert(Relation rel, PgStat_Counter n) { if (pgstat_should_count_relation(rel)) { PgStat_TableStatus *pgstat_info = rel->pgstat_info; ensure_tabstat_xact_level(pgstat_info); pgstat_info->trans->tuples_inserted += n; } } /* * count a tuple update */ void pgstat_count_heap_update(Relation rel, bool hot, bool newpage) { Assert(!(hot && newpage)); if (pgstat_should_count_relation(rel)) { PgStat_TableStatus *pgstat_info = rel->pgstat_info; ensure_tabstat_xact_level(pgstat_info); pgstat_info->trans->tuples_updated++; /* * tuples_hot_updated and tuples_newpage_updated counters are * nontransactional, so just advance them */ if (hot) pgstat_info->counts.tuples_hot_updated++; else if (newpage) pgstat_info->counts.tuples_newpage_updated++; } } /* * count a tuple deletion */ void pgstat_count_heap_delete(Relation rel) { if (pgstat_should_count_relation(rel)) { PgStat_TableStatus *pgstat_info = rel->pgstat_info; ensure_tabstat_xact_level(pgstat_info); pgstat_info->trans->tuples_deleted++; } } /* * update tuple counters due to truncate */ void pgstat_count_truncate(Relation rel) { if (pgstat_should_count_relation(rel)) { PgStat_TableStatus *pgstat_info = rel->pgstat_info; ensure_tabstat_xact_level(pgstat_info); save_truncdrop_counters(pgstat_info->trans, false); pgstat_info->trans->tuples_inserted = 0; pgstat_info->trans->tuples_updated = 0; pgstat_info->trans->tuples_deleted = 0; } } /* * update dead-tuples count * * The semantics of this are that we are reporting the nontransactional * recovery of "delta" dead tuples; so delta_dead_tuples decreases * rather than increasing, and the change goes straight into the per-table * counter, not into transactional state. */ void pgstat_update_heap_dead_tuples(Relation rel, int delta) { if (pgstat_should_count_relation(rel)) { PgStat_TableStatus *pgstat_info = rel->pgstat_info; pgstat_info->counts.delta_dead_tuples -= delta; } } /* * Support function for the SQL-callable pgstat* functions. Returns * the collected statistics for one table or NULL. NULL doesn't mean * that the table doesn't exist, just that there are no statistics, so the * caller is better off to report ZERO instead. */ PgStat_StatTabEntry * pgstat_fetch_stat_tabentry(Oid relid) { return pgstat_fetch_stat_tabentry_ext(IsSharedRelation(relid), relid); } /* * More efficient version of pgstat_fetch_stat_tabentry(), allowing to specify * whether the to-be-accessed table is a shared relation or not. */ PgStat_StatTabEntry * pgstat_fetch_stat_tabentry_ext(bool shared, Oid reloid) { Oid dboid = (shared ? InvalidOid : MyDatabaseId); return (PgStat_StatTabEntry *) pgstat_fetch_entry(PGSTAT_KIND_RELATION, dboid, reloid); } /* * find any existing PgStat_TableStatus entry for rel * * Find any existing PgStat_TableStatus entry for rel_id in the current * database. If not found, try finding from shared tables. * * If an entry is found, copy it and increment the copy's counters with their * subtransaction counterparts, then return the copy. The caller may need to * pfree() the copy. * * If no entry found, return NULL, don't create a new one. */ PgStat_TableStatus * find_tabstat_entry(Oid rel_id) { PgStat_EntryRef *entry_ref; PgStat_TableXactStatus *trans; PgStat_TableStatus *tabentry = NULL; PgStat_TableStatus *tablestatus = NULL; entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION, MyDatabaseId, rel_id); if (!entry_ref) { entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION, InvalidOid, rel_id); if (!entry_ref) return tablestatus; } tabentry = (PgStat_TableStatus *) entry_ref->pending; tablestatus = palloc(sizeof(PgStat_TableStatus)); *tablestatus = *tabentry; /* * Reset tablestatus->trans in the copy of PgStat_TableStatus as it may * point to a shared memory area. Its data is saved below, so removing it * does not matter. */ tablestatus->trans = NULL; /* * Live subtransaction counts are not included yet. This is not a hot * code path so reconcile tuples_inserted, tuples_updated and * tuples_deleted even if the caller may not be interested in this data. */ for (trans = tabentry->trans; trans != NULL; trans = trans->upper) { tablestatus->counts.tuples_inserted += trans->tuples_inserted; tablestatus->counts.tuples_updated += trans->tuples_updated; tablestatus->counts.tuples_deleted += trans->tuples_deleted; } return tablestatus; } /* * Perform relation stats specific end-of-transaction work. Helper for * AtEOXact_PgStat. * * Transfer transactional insert/update counts into the base tabstat entries. * We don't bother to free any of the transactional state, since it's all in * TopTransactionContext and will go away anyway. */ void AtEOXact_PgStat_Relations(PgStat_SubXactStatus *xact_state, bool isCommit) { PgStat_TableXactStatus *trans; for (trans = xact_state->first; trans != NULL; trans = trans->next) { PgStat_TableStatus *tabstat; Assert(trans->nest_level == 1); Assert(trans->upper == NULL); tabstat = trans->parent; Assert(tabstat->trans == trans); /* restore pre-truncate/drop stats (if any) in case of aborted xact */ if (!isCommit) restore_truncdrop_counters(trans); /* count attempted actions regardless of commit/abort */ tabstat->counts.tuples_inserted += trans->tuples_inserted; tabstat->counts.tuples_updated += trans->tuples_updated; tabstat->counts.tuples_deleted += trans->tuples_deleted; if (isCommit) { tabstat->counts.truncdropped = trans->truncdropped; if (trans->truncdropped) { /* forget live/dead stats seen by backend thus far */ tabstat->counts.delta_live_tuples = 0; tabstat->counts.delta_dead_tuples = 0; } /* insert adds a live tuple, delete removes one */ tabstat->counts.delta_live_tuples += trans->tuples_inserted - trans->tuples_deleted; /* update and delete each create a dead tuple */ tabstat->counts.delta_dead_tuples += trans->tuples_updated + trans->tuples_deleted; /* insert, update, delete each count as one change event */ tabstat->counts.changed_tuples += trans->tuples_inserted + trans->tuples_updated + trans->tuples_deleted; } else { /* inserted tuples are dead, deleted tuples are unaffected */ tabstat->counts.delta_dead_tuples += trans->tuples_inserted + trans->tuples_updated; /* an aborted xact generates no changed_tuple events */ } tabstat->trans = NULL; } } /* * Perform relation stats specific end-of-sub-transaction work. Helper for * AtEOSubXact_PgStat. * * Transfer transactional insert/update counts into the next higher * subtransaction state. */ void AtEOSubXact_PgStat_Relations(PgStat_SubXactStatus *xact_state, bool isCommit, int nestDepth) { PgStat_TableXactStatus *trans; PgStat_TableXactStatus *next_trans; for (trans = xact_state->first; trans != NULL; trans = next_trans) { PgStat_TableStatus *tabstat; next_trans = trans->next; Assert(trans->nest_level == nestDepth); tabstat = trans->parent; Assert(tabstat->trans == trans); if (isCommit) { if (trans->upper && trans->upper->nest_level == nestDepth - 1) { if (trans->truncdropped) { /* propagate the truncate/drop status one level up */ save_truncdrop_counters(trans->upper, false); /* replace upper xact stats with ours */ trans->upper->tuples_inserted = trans->tuples_inserted; trans->upper->tuples_updated = trans->tuples_updated; trans->upper->tuples_deleted = trans->tuples_deleted; } else { trans->upper->tuples_inserted += trans->tuples_inserted; trans->upper->tuples_updated += trans->tuples_updated; trans->upper->tuples_deleted += trans->tuples_deleted; } tabstat->trans = trans->upper; pfree(trans); } else { /* * When there isn't an immediate parent state, we can just * reuse the record instead of going through a palloc/pfree * pushup (this works since it's all in TopTransactionContext * anyway). We have to re-link it into the parent level, * though, and that might mean pushing a new entry into the * pgStatXactStack. */ PgStat_SubXactStatus *upper_xact_state; upper_xact_state = pgstat_get_xact_stack_level(nestDepth - 1); trans->next = upper_xact_state->first; upper_xact_state->first = trans; trans->nest_level = nestDepth - 1; } } else { /* * On abort, update top-level tabstat counts, then forget the * subtransaction */ /* first restore values obliterated by truncate/drop */ restore_truncdrop_counters(trans); /* count attempted actions regardless of commit/abort */ tabstat->counts.tuples_inserted += trans->tuples_inserted; tabstat->counts.tuples_updated += trans->tuples_updated; tabstat->counts.tuples_deleted += trans->tuples_deleted; /* inserted tuples are dead, deleted tuples are unaffected */ tabstat->counts.delta_dead_tuples += trans->tuples_inserted + trans->tuples_updated; tabstat->trans = trans->upper; pfree(trans); } } } /* * Generate 2PC records for all the pending transaction-dependent relation * stats. */ void AtPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state) { PgStat_TableXactStatus *trans; for (trans = xact_state->first; trans != NULL; trans = trans->next) { PgStat_TableStatus *tabstat PG_USED_FOR_ASSERTS_ONLY; TwoPhasePgStatRecord record; Assert(trans->nest_level == 1); Assert(trans->upper == NULL); tabstat = trans->parent; Assert(tabstat->trans == trans); record.tuples_inserted = trans->tuples_inserted; record.tuples_updated = trans->tuples_updated; record.tuples_deleted = trans->tuples_deleted; record.inserted_pre_truncdrop = trans->inserted_pre_truncdrop; record.updated_pre_truncdrop = trans->updated_pre_truncdrop; record.deleted_pre_truncdrop = trans->deleted_pre_truncdrop; record.id = tabstat->id; record.shared = tabstat->shared; record.truncdropped = trans->truncdropped; RegisterTwoPhaseRecord(TWOPHASE_RM_PGSTAT_ID, 0, &record, sizeof(TwoPhasePgStatRecord)); } } /* * All we need do here is unlink the transaction stats state from the * nontransactional state. The nontransactional action counts will be * reported to the stats system immediately, while the effects on live and * dead tuple counts are preserved in the 2PC state file. * * Note: AtEOXact_PgStat_Relations is not called during PREPARE. */ void PostPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state) { PgStat_TableXactStatus *trans; for (trans = xact_state->first; trans != NULL; trans = trans->next) { PgStat_TableStatus *tabstat; tabstat = trans->parent; tabstat->trans = NULL; } } /* * 2PC processing routine for COMMIT PREPARED case. * * Load the saved counts into our local pgstats state. */ void pgstat_twophase_postcommit(TransactionId xid, uint16 info, void *recdata, uint32 len) { TwoPhasePgStatRecord *rec = (TwoPhasePgStatRecord *) recdata; PgStat_TableStatus *pgstat_info; /* Find or create a tabstat entry for the rel */ pgstat_info = pgstat_prep_relation_pending(rec->id, rec->shared); /* Same math as in AtEOXact_PgStat, commit case */ pgstat_info->counts.tuples_inserted += rec->tuples_inserted; pgstat_info->counts.tuples_updated += rec->tuples_updated; pgstat_info->counts.tuples_deleted += rec->tuples_deleted; pgstat_info->counts.truncdropped = rec->truncdropped; if (rec->truncdropped) { /* forget live/dead stats seen by backend thus far */ pgstat_info->counts.delta_live_tuples = 0; pgstat_info->counts.delta_dead_tuples = 0; } pgstat_info->counts.delta_live_tuples += rec->tuples_inserted - rec->tuples_deleted; pgstat_info->counts.delta_dead_tuples += rec->tuples_updated + rec->tuples_deleted; pgstat_info->counts.changed_tuples += rec->tuples_inserted + rec->tuples_updated + rec->tuples_deleted; } /* * 2PC processing routine for ROLLBACK PREPARED case. * * Load the saved counts into our local pgstats state, but treat them * as aborted. */ void pgstat_twophase_postabort(TransactionId xid, uint16 info, void *recdata, uint32 len) { TwoPhasePgStatRecord *rec = (TwoPhasePgStatRecord *) recdata; PgStat_TableStatus *pgstat_info; /* Find or create a tabstat entry for the rel */ pgstat_info = pgstat_prep_relation_pending(rec->id, rec->shared); /* Same math as in AtEOXact_PgStat, abort case */ if (rec->truncdropped) { rec->tuples_inserted = rec->inserted_pre_truncdrop; rec->tuples_updated = rec->updated_pre_truncdrop; rec->tuples_deleted = rec->deleted_pre_truncdrop; } pgstat_info->counts.tuples_inserted += rec->tuples_inserted; pgstat_info->counts.tuples_updated += rec->tuples_updated; pgstat_info->counts.tuples_deleted += rec->tuples_deleted; pgstat_info->counts.delta_dead_tuples += rec->tuples_inserted + rec->tuples_updated; } /* * Flush out pending stats for the entry * * If nowait is true, this function returns false if lock could not * immediately acquired, otherwise true is returned. * * Some of the stats are copied to the corresponding pending database stats * entry when successfully flushing. */ bool pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) { static const PgStat_TableCounts all_zeroes; Oid dboid; PgStat_TableStatus *lstats; /* pending stats entry */ PgStatShared_Relation *shtabstats; PgStat_StatTabEntry *tabentry; /* table entry of shared stats */ PgStat_StatDBEntry *dbentry; /* pending database entry */ dboid = entry_ref->shared_entry->key.dboid; lstats = (PgStat_TableStatus *) entry_ref->pending; shtabstats = (PgStatShared_Relation *) entry_ref->shared_stats; /* * Ignore entries that didn't accumulate any actual counts, such as * indexes that were opened by the planner but not used. */ if (memcmp(&lstats->counts, &all_zeroes, sizeof(PgStat_TableCounts)) == 0) { return true; } if (!pgstat_lock_entry(entry_ref, nowait)) return false; /* add the values to the shared entry. */ tabentry = &shtabstats->stats; tabentry->numscans += lstats->counts.numscans; if (lstats->counts.numscans) { TimestampTz t = GetCurrentTransactionStopTimestamp(); if (t > tabentry->lastscan) tabentry->lastscan = t; } tabentry->tuples_returned += lstats->counts.tuples_returned; tabentry->tuples_fetched += lstats->counts.tuples_fetched; tabentry->tuples_inserted += lstats->counts.tuples_inserted; tabentry->tuples_updated += lstats->counts.tuples_updated; tabentry->tuples_deleted += lstats->counts.tuples_deleted; tabentry->tuples_hot_updated += lstats->counts.tuples_hot_updated; tabentry->tuples_newpage_updated += lstats->counts.tuples_newpage_updated; /* * If table was truncated/dropped, first reset the live/dead counters. */ if (lstats->counts.truncdropped) { tabentry->live_tuples = 0; tabentry->dead_tuples = 0; tabentry->ins_since_vacuum = 0; } tabentry->live_tuples += lstats->counts.delta_live_tuples; tabentry->dead_tuples += lstats->counts.delta_dead_tuples; tabentry->mod_since_analyze += lstats->counts.changed_tuples; tabentry->ins_since_vacuum += lstats->counts.tuples_inserted; tabentry->blocks_fetched += lstats->counts.blocks_fetched; tabentry->blocks_hit += lstats->counts.blocks_hit; /* Clamp live_tuples in case of negative delta_live_tuples */ tabentry->live_tuples = Max(tabentry->live_tuples, 0); /* Likewise for dead_tuples */ tabentry->dead_tuples = Max(tabentry->dead_tuples, 0); pgstat_unlock_entry(entry_ref); /* The entry was successfully flushed, add the same to database stats */ dbentry = pgstat_prep_database_pending(dboid); dbentry->tuples_returned += lstats->counts.tuples_returned; dbentry->tuples_fetched += lstats->counts.tuples_fetched; dbentry->tuples_inserted += lstats->counts.tuples_inserted; dbentry->tuples_updated += lstats->counts.tuples_updated; dbentry->tuples_deleted += lstats->counts.tuples_deleted; dbentry->blocks_fetched += lstats->counts.blocks_fetched; dbentry->blocks_hit += lstats->counts.blocks_hit; return true; } void pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref) { PgStat_TableStatus *pending = (PgStat_TableStatus *) entry_ref->pending; if (pending->relation) pgstat_unlink_relation(pending->relation); } /* * Find or create a PgStat_TableStatus entry for rel. New entry is created and * initialized if not exists. */ static PgStat_TableStatus * pgstat_prep_relation_pending(Oid rel_id, bool isshared) { PgStat_EntryRef *entry_ref; PgStat_TableStatus *pending; entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_RELATION, isshared ? InvalidOid : MyDatabaseId, rel_id, NULL); pending = entry_ref->pending; pending->id = rel_id; pending->shared = isshared; return pending; } /* * add a new (sub)transaction state record */ static void add_tabstat_xact_level(PgStat_TableStatus *pgstat_info, int nest_level) { PgStat_SubXactStatus *xact_state; PgStat_TableXactStatus *trans; /* * If this is the first rel to be modified at the current nest level, we * first have to push a transaction stack entry. */ xact_state = pgstat_get_xact_stack_level(nest_level); /* Now make a per-table stack entry */ trans = (PgStat_TableXactStatus *) MemoryContextAllocZero(TopTransactionContext, sizeof(PgStat_TableXactStatus)); trans->nest_level = nest_level; trans->upper = pgstat_info->trans; trans->parent = pgstat_info; trans->next = xact_state->first; xact_state->first = trans; pgstat_info->trans = trans; } /* * Add a new (sub)transaction record if needed. */ static void ensure_tabstat_xact_level(PgStat_TableStatus *pgstat_info) { int nest_level = GetCurrentTransactionNestLevel(); if (pgstat_info->trans == NULL || pgstat_info->trans->nest_level != nest_level) add_tabstat_xact_level(pgstat_info, nest_level); } /* * Whenever a table is truncated/dropped, we save its i/u/d counters so that * they can be cleared, and if the (sub)xact that executed the truncate/drop * later aborts, the counters can be restored to the saved (pre-truncate/drop) * values. * * Note that for truncate we do this on the first truncate in any particular * subxact level only. */ static void save_truncdrop_counters(PgStat_TableXactStatus *trans, bool is_drop) { if (!trans->truncdropped || is_drop) { trans->inserted_pre_truncdrop = trans->tuples_inserted; trans->updated_pre_truncdrop = trans->tuples_updated; trans->deleted_pre_truncdrop = trans->tuples_deleted; trans->truncdropped = true; } } /* * restore counters when a truncate aborts */ static void restore_truncdrop_counters(PgStat_TableXactStatus *trans) { if (trans->truncdropped) { trans->tuples_inserted = trans->inserted_pre_truncdrop; trans->tuples_updated = trans->updated_pre_truncdrop; trans->tuples_deleted = trans->deleted_pre_truncdrop; } }