pax_global_header00006660000000000000000000000064152105113430014505gustar00rootroot0000000000000052 comment=e82aba9678a3ade1de7c5c17feda8b10d5b35de0 pganalyze-pg_stat_plans-e82aba9/000077500000000000000000000000001521051134300170305ustar00rootroot00000000000000pganalyze-pg_stat_plans-e82aba9/.github/000077500000000000000000000000001521051134300203705ustar00rootroot00000000000000pganalyze-pg_stat_plans-e82aba9/.github/workflows/000077500000000000000000000000001521051134300224255ustar00rootroot00000000000000pganalyze-pg_stat_plans-e82aba9/.github/workflows/ci.yml000066400000000000000000000047021521051134300235460ustar00rootroot00000000000000name: CI on: push: branches: ["main"] pull_request: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: name: PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - pg: 16 - pg: 17 - pg: 18 - pg: 19 branch: master steps: - name: Check out code uses: actions/checkout@v6 - name: Install build dependencies run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ build-essential pkg-config libzstd-dev libssl-dev libkrb5-dev - name: Install PostgreSQL ${{ matrix.pg }} if: ${{ !matrix.branch }} run: | sudo apt-get install -y --no-install-recommends postgresql-common sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y sudo apt-get install -y --no-install-recommends \ postgresql-${{ matrix.pg }} postgresql-server-dev-${{ matrix.pg }} echo "PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config" >> "$GITHUB_ENV" - name: Build PostgreSQL from source if: ${{ matrix.branch }} env: PGPREFIX: ${{ github.workspace }}/pginst run: | sudo apt-get install -y --no-install-recommends bison flex perl git clone --depth 1 --branch ${{ matrix.branch }} \ https://github.com/postgres/postgres.git pgsrc cd pgsrc ./configure --prefix="$PGPREFIX" \ --enable-debug --enable-cassert \ --without-icu --without-readline --without-zlib make -j"$(nproc)" make install # pg_plan_advice is needed for the plan_advice regression test make -C contrib/pg_plan_advice install || true echo "PG_CONFIG=$PGPREFIX/bin/pg_config" >> "$GITHUB_ENV" - name: Build pg_stat_plans run: make PG_CONFIG="$PG_CONFIG" - name: Install pg_stat_plans run: sudo make PG_CONFIG="$PG_CONFIG" install - name: Run regression tests run: make PG_CONFIG="$PG_CONFIG" localcheck - name: Upload regression diffs on failure if: failure() uses: actions/upload-artifact@v7 with: name: regression-diffs-pg${{ matrix.pg }} path: | regression.diffs regression.out if-no-files-found: ignore pganalyze-pg_stat_plans-e82aba9/.gitignore000066400000000000000000000005351521051134300210230ustar00rootroot00000000000000# Global excludes across all subdirectories *.o *.obj *.bc *.so *.so.[0-9] *.so.[0-9].[0-9] *.so.[0-9].[0-9][0-9] *.dylib *.dll *.exp *.a *.mo *.pot objfiles.txt .deps/ *.gcno *.gcda *.gcov *.gcov.out lcov*.info coverage/ coverage-html-stamp *.vcproj *.vcxproj win32ver.rc *.exe lib*dll.def lib*.pc # Extension specific excludes log/ results/*.out pganalyze-pg_stat_plans-e82aba9/CHANGELOG.md000066400000000000000000000033571521051134300206510ustar00rootroot00000000000000# Changelog ## 2.1.0 2026-06-05 * Postgres 19 support * Optionally capture plan advice string on Postgres 19 - When enabled with the new pg_stat_plans.plan_advice setting, plan advice will be tracked during each query's planning cycle, and the generated advice will be stored in shared memory for the first execution of a given plan ID. This is similar to running EXPLAIN (PLAN_ADVICE) on the query and helps determine the advice strings that can reproduce a given plan. * Add pg_stat_plans.max_plan_memory setting to total plan text memory - Previously the limit was implicitly calculated from the max * max_size, but plan text sizes can be quite uneven, and its easier to reason about a fixed limit. Together with this change, rework how the plan text limit is implemented, by relying on dsa_set_size_limit. - The new default limit is 16MB, up from the prior implicit 10MB. Due to max_size now being uncoupled from the actual limit, also raise the default max_size to 8kb of (potentially compressed) plan text. This default is chosen to optimize for the DSA handling of "smaller entries" that avoids allocating full DSA pages. Entries that go over the limit will have a blank value for plan text. * Correctness/scaling improvements - Use pending statistics correctly to fix data race - Prevent concurrent garbage collection cycles - Eagerly free plan text when dropping entries to support resurrection * Maintenance improvements - Regenerate jumble funcs from Postgres sources using script - Avoid unused warning for _jumblElements, drop RecordConstLocation - Fix compiler warnings - Fix regression tests for Postgres 16+17, move pgstat_custom for clarity ## 2.0.0 2025-09-11 * Initial release pganalyze-pg_stat_plans-e82aba9/LICENSE000066400000000000000000000043341521051134300200410ustar00rootroot00000000000000Copyright (c) 2025, Duboce Labs, Inc. (pganalyze) Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL DUBOCE LABS, INC. BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF DUBOCE LABS, INC. HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. DUBOCE LABS, INC. SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND DUBOCE LABS, INC. HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. --- PostgreSQL server code (jumblefuncs.*) incorporated under the PostgreSQL license: PostgreSQL Database Management System (formerly known as Postgres, then as Postgres95) Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group Portions Copyright (c) 1994, The Regents of the University of California Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pganalyze-pg_stat_plans-e82aba9/Makefile000066400000000000000000000045631521051134300205000ustar00rootroot00000000000000# contrib/pg_stat_plans/Makefile MODULE_big = pg_stat_plans OBJS = \ $(WIN32RES) \ pg_stat_plans.o \ jumblefuncs.o EXTENSION = pg_stat_plans DATA = pg_stat_plans--2.0.sql pg_stat_plans--2.0--2.1.sql PGFILEDESC = "pg_stat_plans - track per-plan call counts, execution times and EXPLAIN texts" LDFLAGS_SL += $(filter -lm, $(LIBS)) REGRESS_OPTS = --temp-config $(srcdir)/pg_stat_plans.conf REGRESS = select activity privileges cleanup PG_CFLAGS = $(shell pkg-config --cflags libzstd) $(shell pkg-config --cflags openssl) PG_LDFLAGS = $(shell pkg-config --libs libzstd) PG_CONFIG = pg_config ifneq (,$(findstring PostgreSQL 16,$(shell $(PG_CONFIG) --version))) REGRESS_OPTS += --expecteddir=$(PWD)/compat_16_17 OBJS += compat_16_17/pgstat_custom.o endif ifneq (,$(findstring PostgreSQL 17,$(shell $(PG_CONFIG) --version))) REGRESS_OPTS += --expecteddir=$(PWD)/compat_16_17 OBJS += compat_16_17/pgstat_custom.o endif ifneq (,$(findstring PostgreSQL 18,$(shell $(PG_CONFIG) --version))) REGRESS_OPTS += --expecteddir=$(PWD)/compat_18 endif # On Postgres 19+, additionally exercise pg_plan_advice collection, but only # when pg_plan_advice is installed (it must be preloaded for the test). When # present, preload both modules for the whole run; the extra module is inert # while pg_stat_plans.plan_advice is "off" (the default), so the other tests # are unaffected. ifneq (,$(findstring PostgreSQL 19,$(shell $(PG_CONFIG) --version))) PLAN_ADVICE_LIB := $(wildcard $(shell $(PG_CONFIG) --pkglibdir)/pg_plan_advice.*) ifneq (,$(PLAN_ADVICE_LIB)) REGRESS_OPTS = --temp-config $(srcdir)/pg_plan_advice.conf REGRESS = select activity privileges plan_advice cleanup endif endif EXTRA_CLEAN = tmp_check results regression.diffs regression.out PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) # Run the regression tests in a throwaway temporary instance created from the # installed binaries, with pg_stat_plans preloaded via pg_stat_plans.conf. # Unlike "make installcheck", this does not need a separately running and # configured server. Like "make installcheck", it expects the extension to be # installed first, so run "make install" (or "sudo make install" for a # system-packaged PostgreSQL) beforehand. .PHONY: localcheck localcheck: $(top_builddir)/src/test/regress/pg_regress \ --temp-instance=./tmp_check \ --bindir='$(bindir)' \ --inputdir=$(srcdir) \ $(REGRESS_OPTS) $(REGRESS) pganalyze-pg_stat_plans-e82aba9/README.md000066400000000000000000000471311521051134300203150ustar00rootroot00000000000000# pg_stat_plans 2.1 - Track per-plan call counts, execution times and EXPLAIN texts in Postgres `pg_stat_plans` is designed for low overhead tracking of aggregate plan statistics in Postgres, by relying on hashing the plan tree with a plan ID calculation. It aims to help identify plan regressions, and get an example plan for each Postgres query run, slow and fast. Additionally, it allows showing the plan for a currently running query. Plan texts are stored in shared memory for efficiency reasons (instead of a local file), with support for `zstd` compression to compress large plan texts. Plans have the same plan IDs when they have the same "plan shape", which intends to match `EXPLAIN (COSTS OFF)`. This extension is optimized for tracking changes in plan shape, but does not aim to track execution statistics for plans, like [auto_explain](https://www.postgresql.org/docs/current/auto-explain.html) can do for outliers. This project is inspired by multiple Postgres community projects, including the original [pg_stat_plans](https://github.com/2ndQuadrant/pg_stat_plans) extension (unmaintained), with a goal of upstreaming parts of this extension into the core Postgres project over time. **Experimental**. May still change in incompatible ways without notice. Not (yet) recommended for production use. ## Supported PostgreSQL versions Requires at least Postgres 16. Showing plans of running queries requires Postgres 18 or newer, due to relying on plan ID tracking per backend ([2a0cd38da5](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=2a0cd38da5ccf70461c51a489ee7d25fcd3f26be)). Uses pluggable cumulative statistics ([7949d95945](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=7949d9594582ab49dee221e1db1aa5401ace49d4)) on Postgres 18. On Postgres 16 and 17 a shim that replicates similar functionality is used. ## Installation You can use `make install` to build and install the extension. This requires having a `pg_config` in your path that references a Postgres 16 or newer installation. You can optionally build with `zstd` support for compressing plan texts in shared memory. After installing, make sure that your Postgres server loads the shared library: ``` shared_preload_libraries = 'pg_stat_plans' # Optionally, enable zstd compression for plan texts pg_stat_plans.compress = 'zstd' ``` Whilst `pg_stat_statements` is not directly required to use `pg_stat_plans`, you will likely want that in practice to make effective use of this extension. ## Usage Once enabled, the `pg_stat_plans` extension will track cumulative plan statistics on all databases on your Postgres database server. In order to query the collected plan statistics, access the `pg_stat_plans` view: ```sql SELECT * FROM pg_stat_plans; ``` ``` -[ RECORD 1 ]---+---------------------------------------------------------------------------------------------------------------------------- userid | 10 dbid | 16391 toplevel | t queryid | -2322344003805516737 planid | -1865871893278385236 calls | 1 total_exec_time | 0.047708 plan | Limit + | -> Sort + | Sort Key: database_stats_35d.frozenxid_age DESC + | -> Bitmap Heap Scan on database_stats_35d_20250514 database_stats_35d + | Recheck Cond: (server_id = '00000000-0000-0000-0000-000000000000'::uuid) + | Filter: ((frozenxid_age IS NOT NULL) AND (collected_at = '2025-05-14 14:30:00'::timestamp without time zone))+ | -> Bitmap Index Scan on database_stats_35d_20250514_server_id_idx + | Index Cond: (server_id = '00000000-0000-0000-0000-000000000000'::uuid) ``` If you are only interested in the statistics, you can alternatively call `pg_stat_plans(false)` to omit the plan text: ```sql SELECT * FROM pg_stat_plans(false); ``` ``` userid | dbid | toplevel | queryid | planid | calls | total_exec_time | plan --------+-------+----------+----------------------+----------------------+-------+-----------------+------ 10 | 16391 | t | -5621848818004107520 | 6961434712743557023 | 1 | 0.039874 | 10 | 16391 | t | -2441310672058481123 | -2196946116021194031 | 1 | 0.137792 | 10 | 16391 | t | -6930725455674591191 | -2072755433191687359 | 1 | 0.199792 | 426625 | 5 | t | -8648076524241661623 | 3162221630963173795 | 2 | 2.409084 | 426625 | 5 | t | 8478736882705947225 | -45743379005492998 | 3 | 7.022666 | (5 rows) ``` **Important:** Due to using the Postgres cumulative statistics system, statistics counters in all current Postgres versions are only flushed at transaction end with an up to 60 second delay to avoid lock contention. Entries will still be present with their plan text, but with call counts not yet updated. Work is ongoing upstream to improve this in future Postgres releases. You can also group by `queryid` retrieved from `pg_stat_statements`, to get the different plans chosen for the same query. For example, we can see different plans being chosen based on whether a table was expected to have data or not, and Postgres falling back to a sequential scan and in efficient Hash Join incorrectly: ```sql SELECT queryid, query FROM pg_stat_statements WHERE queryid = -7079927730720784986; ``` ``` -[ RECORD 1 ]------------------------------------------------------------------------------------------------------------------------- queryid | -7079927730720784986 query | INSERT INTO schema_column_stats_7d ( + | database_id, table_id, analyzed_at, position, inherited, null_frac, avg_width, n_distinct, correlation + | ) + | ... + | WHERE NOT EXISTS ( + | SELECT $12 FROM schema_column_stats_7d s + | WHERE (s.table_id, s.analyzed_at) = (input.table_id, greatest(input.analyzed_at, date_trunc($13, $10::timestamptz)))+ | ) ``` ```sql SELECT planid, calls, total_exec_time / calls avgtime, plan FROM pg_stat_plans WHERE queryid = -7079927730720784986; ``` ``` -[ RECORD 1 ]--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- planid | 7066245182124090635 calls | 4 avgtime | 0.010312250000000002 plan | Insert on schema_column_stats_7d + | -> Nested Loop Anti Join + | -> Function Scan on input + | -> Append + | -> Index Only Scan using schema_column_stats_7d_20250505_pkey on schema_column_stats_7d_20250505 s_6 + | Index Cond: ((table_id = input.table_id) AND (analyzed_at = GREATEST(input.analyzed_at, date_trunc('day'::text, '2025-05-07 14:30:00+00'::timestamp with time zone))))+ | -> Index Only Scan using schema_column_stats_7d_20250506_pkey on schema_column_stats_7d_20250506 s_7 + | Index Cond: ((table_id = input.table_id) AND (analyzed_at = GREATEST(input.analyzed_at, date_trunc('day'::text, '2025-05-07 14:30:00+00'::timestamp with time zone))))+ | -> Index Only Scan using schema_column_stats_7d_20250507_pkey on schema_column_stats_7d_20250507 s_8 + | Index Cond: ((table_id = input.table_id) AND (analyzed_at = GREATEST(input.analyzed_at, date_trunc('day'::text, '2025-05-07 14:30:00+00'::timestamp with time zone))))+ -[ RECORD 2 ]--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- planid | 6144932094104289715 calls | 2 avgtime | 18.104062499999998 plan | Insert on schema_column_stats_7d + | -> Hash Anti Join + | Hash Cond: (input.table_id = s.table_id) + | Join Filter: (s.analyzed_at = GREATEST(input.analyzed_at, date_trunc('day'::text, '2025-05-07 14:30:00+00'::timestamp with time zone))) + | -> Function Scan on input + | -> Hash + | -> Append + | -> Index Only Scan using schema_column_stats_7d_20250505_pkey on schema_column_stats_7d_20250505 s_6 + | -> Seq Scan on schema_column_stats_7d_20250506 s_7 + | -> Seq Scan on schema_column_stats_7d_20250507 s_8 + ``` Plans will be shown for both currently running queries, as well as those that have finished execution. The call count is updated after execution ends. You can reset all plan statistics and texts by running `pg_stat_plan_reset`: ```sql SELECT pg_stat_plans_reset(); ``` On Postgres 18 and newer, you can retrieve the plan ID and plans for currently running queries through the `pg_stat_plans_activity` view: ```sql SELECT * FROM pg_stat_plans_activity; ``` ``` pid | plan_id | plan -------+----------------------+------------------------------------------------------------------------------------------------------------------------ 83994 | -5449095327982245076 | Merge Join + | | Merge Cond: ((a.datid = p.dbid) AND (a.usesysid = p.userid) AND (a.query_id = p.queryid) AND (a.plan_id = p.planid))+ | | -> Sort + | | Sort Key: a.datid, a.usesysid, a.query_id, a.plan_id + | | -> Function Scan on pg_stat_plans_get_activity a + | | -> Sort + | | Sort Key: p.dbid, p.userid, p.queryid, p.planid + | | -> Function Scan on pg_stat_plans p + | | Filter: (toplevel IS TRUE) 87168 | 4721228144609632390 | Sort + | | Sort Key: q.id + | | -> Nested Loop + | | -> Index Scan using index_query_runs_on_server_id on query_runs q + | | Index Cond: (server_id = '00000000-0000-0000-0000-000000000000'::uuid) + | | Filter: ((started_at IS NULL) AND (finished_at IS NULL)) + | | -> Index Scan using databases_pkey on databases db + | | Index Cond: (id = q.database_id) 81527 | 3819832514333472635 | Result (3 rows) ``` ## Running tests The built-in regression tests can be run by doing: ``` make install make localcheck ``` The `make localcheck` command runs the regression tests in a throwaway temporary instance, so it needs no separately running server. The extension must be installed first. Use `sudo make install` for system-wide Postgres installed using packages. Alternatively, you can use `make installcheck` to test against an already-running local Postgres that has `pg_stat_plans` in its `shared_preload_libraries`. ## Regenerating the plan jumble support The `pg*_jumblefuncs.*.c` files included by `jumblefuncs.c` are generated using `src/backend/nodes/gen_node_support.pl` helper in the Postgres source tree, with modifications applied to jumble (i.e. hash) Plan trees and calculate the plan ID. The source branches that are used for this extraction can be found here: | PostgreSQL | Branch | |------------|--------| | 16 | [pg-stat-plans-16](https://github.com/lfittl/postgres/tree/pg-stat-plans-16) | | 17 | [pg-stat-plans-17](https://github.com/lfittl/postgres/tree/pg-stat-plans-17) | | 18 | [pg-stat-plans-18](https://github.com/lfittl/postgres/tree/pg-stat-plans-18) | | 19 | [pg-stat-plans-19](https://github.com/lfittl/postgres/tree/pg-stat-plans-19) | The principles behind which fields are jumbled are [documented in the Postgres wiki](https://wiki.postgresql.org/wiki/Plan_ID_Jumbling). To regenerate the files, run the following, pointing to a local checkout of the right branch, and using the correct prefix to be used for the files: ``` ./generate_jumblefuncs.sh pg18 /path/to/postgres ``` ## Configuration | setting | possible values | default | description | |-------------------------------|-----------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| | pg_stat_plans.max | 100 - INT_MAX/2 | 5000 | Sets the maximum number of plans tracked by pg_stat_plans in shared memory. | | pg_stat_plans.max_size | 100 - 1048576 | 8192 | Sets the maximum size of an individual plan text (in bytes) tracked by pg_stat_plans. | | pg_stat_plans.max_plan_memory | 256kB - 2TB | 16MB | Sets the total memory limit for plan text storage. When exhausted, new plans are still tracked but without their plan text. | | pg_stat_plans.track | top
all | top | Selects which plans are tracked by pg_stat_plans. | | pg_stat_plans.compress | none
zstd | none | Select compression used by pg_stat_plans. | | pg_stat_plans.plan_advice | off
on | off | Collects a `pg_plan_advice` string for each tracked plan on Postgres 19 or newer with `pg_plan_advice` loaded via `shared_preload_libraries`. | All configuration settings can be changed with a reload (`SIGHUP`). `max_size`, `track`, `compress` and `plan_advice` can also be set by superusers on a per connection basis. The 8kB default for `max_size` is chosen to keep plan text values under the limit for "smaller sizes" for Postgres dynamic shared memory area (DSA) used for plan texts. Up to that size, plan texts come from pooled, fixed-size superblocks that freed entries reuse efficiently; larger ones are instead allocated in blocks of individual 4kB DSA pages, rounded up from the actual size needed. When plan memory is exhausted, new entries will have empty plan text until the total entry count is reached which triggers a garbage collection cycle, freeing up query text records. ## Known issues * Plan IDs may be different in cases where they should not be - Minor differences in filter / index cond expressions (e.g. an extra type cast) - Different partitions being planned for the same Append/Append Merge node based on changes in schema or input parameters * Plan text compression may have higher CPU overhead than necessary - Plan text is always compressed (if setting is enabled), but this likely needs a minimum threshold to reduce overhead - Explore/benchmark alternate compression methods (e.g. lz4 for lower CPU overhead) ## Authors * Lukas Fittl * Marko M. Inspired by earlier work done by Sami Imseih. ## License PostgreSQL server code (jumblefuncs.*) incorporated under the PostgreSQL license
Portions Copyright (c) 1996-2026, The PostgreSQL Global Development Group
Portions Copyright (c) 1994, The Regents of the University of California All other parts are licensed under the PostgreSQL license
Copyright (c) 2026, Duboce Labs, Inc. (pganalyze) See LICENSE file for details. pganalyze-pg_stat_plans-e82aba9/compat_16_17/000077500000000000000000000000001521051134300211305ustar00rootroot00000000000000pganalyze-pg_stat_plans-e82aba9/compat_16_17/expected/000077500000000000000000000000001521051134300227315ustar00rootroot00000000000000pganalyze-pg_stat_plans-e82aba9/compat_16_17/expected/activity.out000066400000000000000000000011411521051134300253130ustar00rootroot00000000000000SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- check if we see our own plan in activity -- SELECT plan FROM pg_stat_plans_activity WHERE pid = pg_backend_pid(); ERROR: Not implemented, use of pg_stat_plans_get_activity requires Postgres 18+ -- -- check if we handle showing our plan for named prepared statements correctly -- PREPARE x AS SELECT plan FROM pg_stat_plans_activity WHERE pid = pg_backend_pid(); EXECUTE x; ERROR: Not implemented, use of pg_stat_plans_get_activity requires Postgres 18+ DEALLOCATE x; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/compat_16_17/expected/cleanup.out000066400000000000000000000000361521051134300251100ustar00rootroot00000000000000DROP EXTENSION pg_stat_plans; pganalyze-pg_stat_plans-e82aba9/compat_16_17/expected/privileges.out000066400000000000000000000101221521051134300256270ustar00rootroot00000000000000-- -- Only superusers and roles with privileges of the pg_read_all_stats role -- are allowed to see the plan text, queryid and planid of queries executed by -- other users. Other users can see the statistics. -- CREATE ROLE regress_stats_superuser SUPERUSER; CREATE ROLE regress_stats_user1; CREATE ROLE regress_stats_user2; GRANT pg_read_all_stats TO regress_stats_user2; SET ROLE regress_stats_superuser; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) SELECT 1 AS "SUPER"; SUPER ------- 1 (1 row) SET ROLE regress_stats_user1; SELECT 1+1 AS "ONE"; ONE ----- 2 (1 row) SET ROLE regress_stats_user2; SELECT 1+1 AS "TWO"; TWO ----- 2 (1 row) -- Wait for pending stats to be flushed SET ROLE regress_stats_superuser; SELECT pg_sleep(1); pg_sleep ---------- (1 row) -- -- regress_stats_user1 has no privileges to read the plan text, queryid -- or planid of queries executed by others but can see statistics -- like calls and rows. -- -- We run this before the other tests so that we don't have a surprising -- "" entry from other roles checking pg_stat_plans. -- SET ROLE regress_stats_user1; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------------------------+------- regress_stats_superuser | | | | 1 regress_stats_superuser | | | | 1 regress_stats_superuser | | | | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | | | | 1 (5 rows) -- -- A superuser can read all columns of queries executed by others, -- including plan text, queryid and planid. -- SET ROLE regress_stats_superuser; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------+------- regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | t | t | Result | 1 (5 rows) -- -- regress_stats_user2, with pg_read_all_stats role privileges, can -- read all columns, including plan text, queryid and planid, of queries -- executed by others. -- SET ROLE regress_stats_user2; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------+------- regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | t | t | Result | 1 (5 rows) -- -- cleanup -- RESET ROLE; DROP ROLE regress_stats_superuser; DROP ROLE regress_stats_user1; DROP ROLE regress_stats_user2; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/compat_16_17/expected/select.out000066400000000000000000000260551521051134300247510ustar00rootroot00000000000000-- -- SELECT statements -- CREATE EXTENSION pg_stat_plans; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- simple statements -- SELECT 1 FROM pg_class LIMIT 1; ?column? ---------- 1 (1 row) SELECT 1 FROM pg_class WHERE relname = 'pg_class'; ?column? ---------- 1 (1 row) SET enable_indexscan = off; SELECT 1 FROM pg_class WHERE relname = 'pg_class'; ?column? ---------- 1 (1 row) SET enable_indexscan = on; -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls --------------------------------------------------------------+------- Bitmap Heap Scan on pg_class +| 1 Recheck Cond: (relname = 'pg_class'::name) +| -> Bitmap Index Scan on pg_class_relname_nsp_index +| Index Cond: (relname = 'pg_class'::name) | Index Only Scan using pg_class_relname_nsp_index on pg_class+| 1 Index Cond: (relname = 'pg_class'::name) | Limit +| 1 -> Seq Scan on pg_class | Result | 1 Result | 1 (5 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- subplans and CTEs -- WITH x AS MATERIALIZED (SELECT 1) SELECT * FROM x; ?column? ---------- 1 (1 row) SELECT a.attname, (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) FROM pg_catalog.pg_attribute a WHERE a.attrelid = 'pg_class'::regclass ORDER BY attnum LIMIT 1; attname | pg_get_expr ----------+------------- tableoid | (1 row) -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls -------------------------------------------------------------------------------+------- CTE Scan on x +| 1 CTE x +| -> Result | Limit +| 1 -> Index Scan using pg_attribute_relid_attnum_index on pg_attribute a +| Index Cond: (attrelid = '1259'::oid) +| SubPlan 1 +| -> Result +| One-Time Filter: a.atthasdef +| -> Seq Scan on pg_attrdef d +| Filter: ((adrelid = a.attrelid) AND (adnum = a.attnum)) | Result | 1 Result | 1 (4 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- partitoning -- create table lp (a char) partition by list (a); create table lp_default partition of lp default; create table lp_ef partition of lp for values in ('e', 'f'); create table lp_ad partition of lp for values in ('a', 'd'); create table lp_bc partition of lp for values in ('b', 'c'); create table lp_g partition of lp for values in ('g'); create table lp_null partition of lp for values in (null); select * from lp; a --- (0 rows) select * from lp where a > 'a' and a < 'd'; a --- (0 rows) select * from lp where a > 'a' and a <= 'd'; a --- (0 rows) select * from lp where a = 'a'; a --- (0 rows) select * from lp where 'a' = a; /* commuted */ a --- (0 rows) select * from lp where a is not null; a --- (0 rows) select * from lp where a is null; a --- (0 rows) select * from lp where a = 'a' or a = 'c'; a --- (0 rows) select * from lp where a is not null and (a = 'a' or a = 'c'); a --- (0 rows) select * from lp where a <> 'g'; a --- (0 rows) select * from lp where a <> 'a' and a <> 'd'; a --- (0 rows) select * from lp where a not in ('a', 'd'); a --- (0 rows) -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls --------------------------------------------------------------------------------+------- Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar)) +| -> Seq Scan on lp_bc lp_2 +| Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar)) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) +| -> Seq Scan on lp_bc lp_2 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) +| -> Seq Scan on lp_default lp_3 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar)))+| -> Seq Scan on lp_bc lp_2 +| Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar))) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_bc lp_2 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_ef lp_3 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_default lp_4 +| Filter: (a <> 'g'::bpchar) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_bc lp_2 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_ef lp_3 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_g lp_4 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_default lp_5 +| Filter: (a IS NOT NULL) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| -> Seq Scan on lp_bc lp_2 +| -> Seq Scan on lp_ef lp_3 +| -> Seq Scan on lp_g lp_4 +| -> Seq Scan on lp_null lp_5 +| -> Seq Scan on lp_default lp_6 | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_ef lp_2 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_g lp_3 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_default lp_4 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: ((a > 'a'::bpchar) AND (a < 'd'::bpchar)) +| -> Seq Scan on lp_default lp_2 +| Filter: ((a > 'a'::bpchar) AND (a < 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_ef lp_2 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_g lp_3 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_default lp_4 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) | Result | 1 Result | 1 Seq Scan on lp_ad lp +| 1 Filter: ('a'::bpchar = a) | Seq Scan on lp_ad lp +| 1 Filter: (a = 'a'::bpchar) | Seq Scan on lp_null lp +| 1 Filter: (a IS NULL) | (14 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/compat_16_17/pgstat_custom.c000066400000000000000000001332611521051134300241760ustar00rootroot00000000000000/*-------------------------------------------------------------------------- * * pgstat_custom.c * Compatibility layer for pluggable cumulative statistics on older * releases. * * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION * pgstat_custom.c * * ------------------------------------------------------------------------- */ #include "postgres.h" #if PG_VERSION_NUM < 180000 #include "access/xact.h" #include "utils/memutils.h" #include "utils/timestamp.h" #include "storage/ipc.h" #include "storage/shmem.h" #include "pgstat_custom.h" /* Range of IDs allowed, for built-in and custom kinds */ #define PGSTAT_KIND_MIN 1 /* Minimum ID allowed */ #define PGSTAT_KIND_MAX 32 /* Maximum ID allowed */ #define PGSTAT_KIND_BUILTIN_MIN 0 #define PGSTAT_KIND_BUILTIN_MAX 23 #define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1) /* Custom stats kinds */ /* Range of IDs allowed for custom stats kinds */ #define PGSTAT_KIND_CUSTOM_MIN 24 #define PGSTAT_KIND_CUSTOM_MAX PGSTAT_KIND_MAX #define PGSTAT_KIND_CUSTOM_SIZE (PGSTAT_KIND_CUSTOM_MAX - PGSTAT_KIND_CUSTOM_MIN + 1) static inline bool pgstat_custom_is_kind_builtin(PgStat_Kind kind) { return kind >= PGSTAT_KIND_BUILTIN_MIN && kind <= PGSTAT_KIND_BUILTIN_MAX; } static inline bool pgstat_custom_is_kind_custom(PgStat_Kind kind) { return kind >= PGSTAT_KIND_CUSTOM_MIN && kind <= PGSTAT_KIND_CUSTOM_MAX; } /* Copied from pgstat.c */ /* ---------- * Timer definitions. * * In milliseconds. * ---------- */ /* minimum interval non-forced stats flushes.*/ #define PGSTAT_MIN_INTERVAL 1000 /* how long until to block flushing pending stats updates */ #define PGSTAT_MAX_INTERVAL 60000 /* when to call pgstat_report_stat() again, even when idle */ #define PGSTAT_IDLE_INTERVAL 10000 static bool pgstat_custom_flush_pending_entries(bool nowait); /* ---------- * state shared with pgstat_*.c * ---------- */ PgStat_LocalState pgStatCustomLocal; /* ---------- * Local data * * NB: There should be only variables related to stats infrastructure here, * not for specific kinds of stats. * ---------- */ /* * Memory contexts containing the pgStatEntryRefHash table, the * pgStatSharedRef entries, and pending data respectively. Mostly to make it * easier to track / attribute memory usage. */ static MemoryContext pgStatCustomPendingContext = NULL; /* * Backend local list of PgStat_EntryRef with unflushed pending stats. * * Newly pending entries should only ever be added to the end of the list, * otherwise pgstat_flush_pending_entries() might not see them immediately. */ static dlist_head pgStatCustomPending = DLIST_STATIC_INIT(pgStatCustomPending); /* * Force the next stats flush to happen regardless of * PGSTAT_MIN_INTERVAL. Useful in test scripts. */ static bool pgStatCustomForceNextFlush = false; /* * For assertions that check pgstat is not used before initialization / after * shutdown. */ #ifdef USE_ASSERT_CHECKING static bool pgstat_custom_is_initialized = false; static bool pgstat_custom_is_shutdown = false; #endif /* * Information about custom statistics kinds. * * These are saved in a different array than the built-in kinds to save * in clarity with the initializations. * * Indexed by PGSTAT_KIND_CUSTOM_MIN, of size PGSTAT_KIND_CUSTOM_SIZE. */ static const PgStat_KindInfo **pgstat_kind_custom_infos = NULL; /* * Note there is no equivalent to pgstat_before_server_shutdown due to missing * hooks to set up an equivalent. Therefore all custom stats are lost on shutdown. */ /* ------------------------------------------------------------ * Backend initialization / shutdown functions * ------------------------------------------------------------ */ /* * Shut down a single backend's statistics reporting at process exit. * * Flush out any remaining statistics counts. Without this, operations * triggered during backend exit (such as temp table deletions) won't be * counted. */ static void pgstat_custom_shutdown_hook(int code, Datum arg) { Assert(!pgstat_custom_is_shutdown); Assert(IsUnderPostmaster || !IsPostmasterEnvironment); /* * If we got as far as discovering our own database ID, we can flush out * what we did so far. Otherwise, we'd be reporting an invalid database * ID, so forget it. (This means that accesses to pg_database during * failed backend starts might never get counted.) */ /*if (OidIsValid(MyDatabaseId)) pgstat_report_disconnect(MyDatabaseId);*/ pgstat_custom_report_stat(true); /* there shouldn't be any pending changes left */ Assert(dlist_is_empty(&pgStatCustomPending)); dlist_init(&pgStatCustomPending); /* drop the backend stats entry */ /*if (!pgstat_drop_entry(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber)) pgstat_request_entry_refs_gc();*/ pgstat_custom_detach_shmem(); #ifdef USE_ASSERT_CHECKING pgstat_custom_is_shutdown = true; #endif } /* * Initialize pgstats state, and set up our on-proc-exit hook. Called from * BaseInit(). * * NOTE: MyDatabaseId isn't set yet; so the shutdown hook has to be careful. */ void pgstat_custom_initialize(void) { Assert(!pgstat_custom_is_initialized); pgstat_custom_attach_shmem(); /*pgstat_custom_init_snapshot_fixed();*/ /* Backend initialization callbacks */ /*for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) { const PgStat_KindInfo *kind_info = pgstat_custom_get_kind_info(kind); if (kind_info == NULL || kind_info->init_backend_cb == NULL) continue; kind_info->init_backend_cb(); }*/ /* Set up a process-exit hook to clean up */ before_shmem_exit(pgstat_custom_shutdown_hook, 0); #ifdef USE_ASSERT_CHECKING pgstat_custom_is_initialized = true; #endif } /* ------------------------------------------------------------ * Public functions used by backends follow * ------------------------------------------------------------ */ /* * Must be called by processes that performs DML: tcop/postgres.c, logical * receiver processes, SPI worker, etc. to flush pending statistics updates to * shared memory. * * Unless called with 'force', pending stats updates are flushed happen once * per PGSTAT_MIN_INTERVAL (1000ms). When not forced, stats flushes do not * block on lock acquisition, except if stats updates have been pending for * longer than PGSTAT_MAX_INTERVAL (60000ms). * * Whenever pending stats updates remain at the end of pgstat_report_stat() a * suggested idle timeout is returned. Currently this is always * PGSTAT_IDLE_INTERVAL (10000ms). Callers can use the returned time to set up * a timeout after which to call pgstat_report_stat(true), but are not * required to do so. * * Note that this is called only when not within a transaction, so it is fair * to use transaction stop time as an approximation of current time. */ long pgstat_custom_report_stat(bool force) { static TimestampTz pending_since = 0; static TimestampTz last_flush = 0; bool partial_flush; TimestampTz now; bool nowait; pgstat_custom_assert_is_up(); //Assert(!IsTransactionOrTransactionBlock()); /* "absorb" the forced flush even if there's nothing to flush */ if (pgStatCustomForceNextFlush) { force = true; pgStatCustomForceNextFlush = false; } /* Don't expend a clock check if nothing to do */ if (dlist_is_empty(&pgStatCustomPending)/* && !pgstat_report_fixed*/) { return 0; } /* * There should never be stats to report once stats are shut down. Can't * assert that before the checks above, as there is an unconditional * pgstat_report_stat() call in pgstat_shutdown_hook() - which at least * the process that ran pgstat_before_server_shutdown() will still call. */ Assert(!pgStatCustomLocal.shmem->is_shutdown); if (force) { /* * Stats reports are forced either when it's been too long since stats * have been reported or in processes that force stats reporting to * happen at specific points (including shutdown). In the former case * the transaction stop time might be quite old, in the latter it * would never get cleared. */ now = GetCurrentTimestamp(); } else { now = GetCurrentTransactionStopTimestamp(); if (pending_since > 0 && TimestampDifferenceExceeds(pending_since, now, PGSTAT_MAX_INTERVAL)) { /* don't keep pending updates longer than PGSTAT_MAX_INTERVAL */ force = true; } else if (last_flush > 0 && !TimestampDifferenceExceeds(last_flush, now, PGSTAT_MIN_INTERVAL)) { /* don't flush too frequently */ if (pending_since == 0) pending_since = now; return PGSTAT_IDLE_INTERVAL; } } /*pgstat_update_dbstats(now);*/ /* don't wait for lock acquisition when !force */ nowait = !force; partial_flush = false; /* flush of variable-numbered stats tracked in pending entries list */ partial_flush |= pgstat_custom_flush_pending_entries(nowait); /* flush of other stats kinds */ /*if (pgstat_report_fixed) { for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) { const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); if (!kind_info) continue; if (!kind_info->flush_static_cb) continue; partial_flush |= kind_info->flush_static_cb(nowait); } }*/ last_flush = now; /* * If some of the pending stats could not be flushed due to lock * contention, let the caller know when to retry. */ if (partial_flush) { /* force should have prevented us from getting here */ Assert(!force); /* remember since when stats have been pending */ if (pending_since == 0) pending_since = now; return PGSTAT_IDLE_INTERVAL; } pending_since = 0; /* pgstat_report_fixed = false;*/ return 0; } /* ------------------------------------------------------------ * Backend-local pending stats infrastructure * ------------------------------------------------------------ */ /* * Returns the appropriate PgStat_EntryRef, preparing it to receive pending * stats if not already done. * * If created_entry is non-NULL, it'll be set to true if the entry is newly * created, false otherwise. */ PgStat_EntryRef * pgstat_custom_prep_pending_entry(PgStat_Kind kind, Oid dboid, Oid objid, bool *created_entry) { PgStat_EntryRef *entry_ref; /* need to be able to flush out */ Assert(pgstat_custom_get_kind_info(kind)->flush_pending_cb != NULL); if (unlikely(!pgStatCustomPendingContext)) { pgStatCustomPendingContext = AllocSetContextCreate(TopMemoryContext, "PgStat Custom Pending", ALLOCSET_SMALL_SIZES); } entry_ref = pgstat_custom_get_entry_ref(kind, dboid, objid, true, created_entry); if (entry_ref->pending == NULL) { size_t entrysize = pgstat_custom_get_kind_info(kind)->pending_size; Assert(entrysize != (size_t) -1); entry_ref->pending = MemoryContextAllocZero(pgStatCustomPendingContext, entrysize); dlist_push_tail(&pgStatCustomPending, &entry_ref->pending_node); } return entry_ref; } /* * Return an existing stats entry, or NULL. * * This should only be used for helper function for pgstatfuncs.c - outside of * that it shouldn't be needed. */ PgStat_EntryRef * pgstat_custom_fetch_pending_entry(PgStat_Kind kind, Oid dboid, Oid objid) { PgStat_EntryRef *entry_ref; entry_ref = pgstat_custom_get_entry_ref(kind, dboid, objid, false, NULL); if (entry_ref == NULL || entry_ref->pending == NULL) return NULL; return entry_ref; } void pgstat_custom_delete_pending_entry(PgStat_EntryRef *entry_ref) { PgStat_Kind kind = entry_ref->shared_entry->key.kind; const PgStat_KindInfo *kind_info = pgstat_custom_get_kind_info(kind); void *pending_data = entry_ref->pending; Assert(pending_data != NULL); /* !fixed_amount stats should be handled explicitly */ Assert(!pgstat_custom_get_kind_info(kind)->fixed_amount); if (kind_info->delete_pending_cb) kind_info->delete_pending_cb(entry_ref); pfree(pending_data); entry_ref->pending = NULL; dlist_delete(&entry_ref->pending_node); } /* * Flush out pending variable-numbered stats. */ static bool pgstat_custom_flush_pending_entries(bool nowait) { bool have_pending = false; dlist_node *cur = NULL; /* * Need to be a bit careful iterating over the list of pending entries. * Processing a pending entry may queue further pending entries to the end * of the list that we want to process, so a simple iteration won't do. * Further complicating matters is that we want to delete the current * entry in each iteration from the list if we flushed successfully. * * So we just keep track of the next pointer in each loop iteration. */ if (!dlist_is_empty(&pgStatCustomPending)) cur = dlist_head_node(&pgStatCustomPending); while (cur) { PgStat_EntryRef *entry_ref = dlist_container(PgStat_EntryRef, pending_node, cur); PgStat_HashKey key = entry_ref->shared_entry->key; PgStat_Kind kind = key.kind; const PgStat_KindInfo *kind_info = pgstat_custom_get_kind_info(kind); bool did_flush; dlist_node *next; Assert(!kind_info->fixed_amount); Assert(kind_info->flush_pending_cb != NULL); /* flush the stats, if possible */ did_flush = kind_info->flush_pending_cb(entry_ref, nowait); Assert(did_flush || nowait); /* determine next entry, before deleting the pending entry */ if (dlist_has_next(&pgStatCustomPending, cur)) next = dlist_next_node(&pgStatCustomPending, cur); else next = NULL; /* if successfully flushed, remove entry */ if (did_flush) pgstat_custom_delete_pending_entry(entry_ref); else have_pending = true; cur = next; } Assert(dlist_is_empty(&pgStatCustomPending) == !have_pending); return have_pending; } /* * Register a new stats kind. * * PgStat_Kinds must be globally unique across all extensions. Refer * to https://wiki.postgresql.org/wiki/CustomCumulativeStats to reserve a * unique ID for your extension, to avoid conflicts with other extension * developers. During development, use PGSTAT_KIND_EXPERIMENTAL to avoid * needlessly reserving a new ID. */ void pgstat_custom_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info) { uint32 idx = kind - PGSTAT_KIND_CUSTOM_MIN; if (kind_info->name == NULL || strlen(kind_info->name) == 0) ereport(ERROR, (errmsg("custom cumulative statistics name is invalid"), errhint("Provide a non-empty name for the custom cumulative statistics."))); if (!pgstat_custom_is_kind_custom(kind)) ereport(ERROR, (errmsg("custom cumulative statistics ID %u is out of range", kind), errhint("Provide a custom cumulative statistics ID between %u and %u.", PGSTAT_KIND_CUSTOM_MIN, PGSTAT_KIND_CUSTOM_MAX))); if (!process_shared_preload_libraries_in_progress) ereport(ERROR, (errmsg("failed to register custom cumulative statistics \"%s\" with ID %u", kind_info->name, kind), errdetail("Custom cumulative statistics must be registered while initializing modules in \"shared_preload_libraries\"."))); /* * Check some data for fixed-numbered stats. */ if (kind_info->fixed_amount) { if (kind_info->shared_size == 0) ereport(ERROR, (errmsg("custom cumulative statistics property is invalid"), errhint("Custom cumulative statistics require a shared memory size for fixed-numbered objects."))); } /* * If pgstat_kind_custom_infos is not available yet, allocate it. */ if (pgstat_kind_custom_infos == NULL) { pgstat_kind_custom_infos = (const PgStat_KindInfo **) MemoryContextAllocZero(TopMemoryContext, sizeof(PgStat_KindInfo *) * PGSTAT_KIND_CUSTOM_SIZE); } if (pgstat_kind_custom_infos[idx] != NULL && pgstat_kind_custom_infos[idx]->name != NULL) ereport(ERROR, (errmsg("failed to register custom cumulative statistics \"%s\" with ID %u", kind_info->name, kind), errdetail("Custom cumulative statistics \"%s\" already registered with the same ID.", pgstat_kind_custom_infos[idx]->name))); /* check for existing custom stats with the same name */ for (PgStat_Kind existing_kind = PGSTAT_KIND_CUSTOM_MIN; existing_kind <= PGSTAT_KIND_CUSTOM_MAX; existing_kind++) { uint32 existing_idx = existing_kind - PGSTAT_KIND_CUSTOM_MIN; if (pgstat_kind_custom_infos[existing_idx] == NULL) continue; if (!pg_strcasecmp(pgstat_kind_custom_infos[existing_idx]->name, kind_info->name)) ereport(ERROR, (errmsg("failed to register custom cumulative statistics \"%s\" with ID %u", kind_info->name, kind), errdetail("Existing cumulative statistics with ID %u has the same name.", existing_kind))); } /* Register it */ pgstat_kind_custom_infos[idx] = kind_info; ereport(LOG, (errmsg("registered custom cumulative statistics \"%s\" with ID %u", kind_info->name, kind))); } /* * Stats should only be reported after pgstat_initialize() and before * pgstat_shutdown(). This check is put in a few central places to catch * violations of this rule more easily. */ #ifdef USE_ASSERT_CHECKING void pgstat_custom_assert_is_up(void) { Assert(pgstat_custom_is_initialized && !pgstat_custom_is_shutdown); } #endif const PgStat_KindInfo * pgstat_custom_get_kind_info(PgStat_Kind kind) { if (pgstat_custom_is_kind_builtin(kind)) elog(ERROR, "Unexpected built-in kind (only custom kind numbers should be used): %d", kind); if (pgstat_custom_is_kind_custom(kind)) { uint32 idx = kind - PGSTAT_KIND_CUSTOM_MIN; if (pgstat_kind_custom_infos == NULL || pgstat_kind_custom_infos[idx] == NULL) return NULL; return pgstat_kind_custom_infos[idx]; } return NULL; } /* Copied from pgstat_shmem.c */ #define PGSTAT_ENTRY_REF_HASH_SIZE 128 /* hash table entry for finding the PgStat_EntryRef for a key */ typedef struct PgStat_EntryRefHashEntry { PgStat_HashKey key; /* hash key */ char status; /* for simplehash use */ PgStat_EntryRef *entry_ref; } PgStat_EntryRefHashEntry; /* for references to shared statistics entries */ #define SH_PREFIX pgstat_custom_entry_ref_hash #define SH_ELEMENT_TYPE PgStat_EntryRefHashEntry #define SH_KEY_TYPE PgStat_HashKey #define SH_KEY key #define SH_HASH_KEY(tb, key) \ pgstat_hash_hash_key(&key, sizeof(PgStat_HashKey), NULL) #define SH_EQUAL(tb, a, b) \ pgstat_cmp_hash_key(&a, &b, sizeof(PgStat_HashKey), NULL) == 0 #define SH_SCOPE static inline #define SH_DEFINE #define SH_DECLARE #include "lib/simplehash.h" /* parameter for the shared hash */ static const dshash_parameters dsh_params = { sizeof(PgStat_HashKey), sizeof(PgStatShared_HashEntry), pgstat_cmp_hash_key, pgstat_hash_hash_key, #if PG_VERSION_NUM >= 170000 dshash_memcpy, #endif LWTRANCHE_PGSTATS_HASH }; static void pgstat_custom_free_entry(PgStatShared_HashEntry *shent, dshash_seq_status *hstat); static void pgstat_custom_release_entry_ref(PgStat_HashKey key, PgStat_EntryRef *entry_ref, bool discard_pending); static bool pgstat_custom_need_entry_refs_gc(void); static void pgstat_custom_gc_entry_refs(void); static void pgstat_custom_release_all_entry_refs(bool discard_pending); typedef bool (*ReleaseMatchCB) (PgStat_EntryRefHashEntry *, Datum data); static void pgstat_custom_release_matching_entry_refs(bool discard_pending, ReleaseMatchCB match, Datum match_data); static void pgstat_custom_setup_memcxt(void); /* * Backend local references to shared stats entries. If there are pending * updates to a stats entry, the PgStat_EntryRef is added to the pgStatPending * list. * * When a stats entry is dropped each backend needs to release its reference * to it before the memory can be released. To trigger that * pgStatCustomLocal.shmem->gc_request_count is incremented - which each backend * compares to their copy of pgStatSharedRefAge on a regular basis. */ static pgstat_custom_entry_ref_hash_hash *pgStatCustomEntryRefHash = NULL; static int pgStatCustomSharedRefAge = 0; /* cache age of pgStatCustomLocal.shmem */ /* * Memory contexts containing the pgStatCustomEntryRefHash table and the * pgStatSharedRef entries respectively. Kept separate to make it easier to * track / attribute memory usage. */ static MemoryContext pgStatCustomSharedRefContext = NULL; static MemoryContext pgStatCustomEntryRefHashContext = NULL; /* ------------------------------------------------------------ * Public functions called from extension init follow * ------------------------------------------------------------ */ /* * The size of the shared memory allocation for stats stored in the shared * stats hash table. This allocation will be done as part of the main shared * memory, rather than dynamic shared memory, allowing it to be initialized in * postmaster. */ static Size pgstat_custom_dsa_init_size(void) { Size sz; /* * The dshash header / initial buckets array needs to fit into "plain" * shared memory, but it's beneficial to not need dsm segments * immediately. A size of 256kB seems works well and is not * disproportional compared to other constant sized shared memory * allocations. NB: To avoid DSMs further, the user can configure * min_dynamic_shared_memory. */ sz = 256 * 1024; Assert(dsa_minimum_size() <= sz); return MAXALIGN(sz); } /* * Compute shared memory space needed for cumulative statistics */ static Size StatsCustomShmemSize(void) { Size sz; sz = MAXALIGN(sizeof(PgStat_ShmemControl)); sz = add_size(sz, pgstat_custom_dsa_init_size()); /* Add shared memory for all the custom fixed-numbered statistics */ for (PgStat_Kind kind = PGSTAT_KIND_CUSTOM_MIN; kind <= PGSTAT_KIND_CUSTOM_MAX; kind++) { const PgStat_KindInfo *kind_info = pgstat_custom_get_kind_info(kind); if (!kind_info) continue; if (!kind_info->fixed_amount) continue; Assert(kind_info->shared_size != 0); sz += MAXALIGN(kind_info->shared_size); } return sz; } /* * Initialize cumulative statistics system during startup */ void StatsCustomShmemInit(void) { bool found; Size sz; sz = StatsCustomShmemSize(); pgStatCustomLocal.shmem = (PgStat_ShmemControl *) ShmemInitStruct("Shared Memory Stats Custom", sz, &found); if (!IsUnderPostmaster) { dsa_area *dsa; dshash_table *dsh; PgStat_ShmemControl *ctl = pgStatCustomLocal.shmem; char *p = (char *) ctl; Assert(!found); /* the allocation of pgStatCustomLocal.shmem itself */ p += MAXALIGN(sizeof(PgStat_ShmemControl)); /* * Create a small dsa allocation in plain shared memory. This is * required because postmaster cannot use dsm segments. It also * provides a small efficiency win. */ ctl->raw_dsa_area = p; p += MAXALIGN(pgstat_custom_dsa_init_size()); dsa = dsa_create_in_place(ctl->raw_dsa_area, pgstat_custom_dsa_init_size(), LWTRANCHE_PGSTATS_DSA, NULL); dsa_pin(dsa); /* * To ensure dshash is created in "plain" shared memory, temporarily * limit size of dsa to the initial size of the dsa. */ dsa_set_size_limit(dsa, pgstat_custom_dsa_init_size()); /* * With the limit in place, create the dshash table. XXX: It'd be nice * if there were dshash_create_in_place(). */ dsh = dshash_create(dsa, &dsh_params, NULL); ctl->hash_handle = dshash_get_hash_table_handle(dsh); /* lift limit set above */ dsa_set_size_limit(dsa, -1); /* * Postmaster will never access these again, thus free the local * dsa/dshash references. */ dshash_detach(dsh); dsa_detach(dsa); pg_atomic_init_u64(&ctl->gc_request_count, 1); /* initialize fixed-numbered stats */ /*for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) { const PgStat_KindInfo *kind_info = pgstat_custom_get_kind_info(kind); char *ptr; if (!kind_info || !kind_info->fixed_amount) continue; if (pgstat_custom_is_kind_builtin(kind)) ptr = ((char *) ctl) + kind_info->shared_ctl_off; else { int idx = kind - PGSTAT_KIND_CUSTOM_MIN; Assert(kind_info->shared_size != 0); ctl->custom_data[idx] = ShmemAlloc(kind_info->shared_size); ptr = ctl->custom_data[idx]; } kind_info->init_shmem_cb(ptr); }*/ } else { Assert(found); } } void pgstat_custom_attach_shmem(void) { MemoryContext oldcontext; Assert(pgStatCustomLocal.dsa == NULL); /* stats shared memory persists for the backend lifetime */ oldcontext = MemoryContextSwitchTo(TopMemoryContext); pgStatCustomLocal.dsa = dsa_attach_in_place(pgStatCustomLocal.shmem->raw_dsa_area, NULL); dsa_pin_mapping(pgStatCustomLocal.dsa); pgStatCustomLocal.shared_hash = dshash_attach(pgStatCustomLocal.dsa, &dsh_params, pgStatCustomLocal.shmem->hash_handle, NULL); MemoryContextSwitchTo(oldcontext); } void pgstat_custom_detach_shmem(void) { Assert(pgStatCustomLocal.dsa); /* we shouldn't leave references to shared stats */ pgstat_custom_release_all_entry_refs(false); dshash_detach(pgStatCustomLocal.shared_hash); pgStatCustomLocal.shared_hash = NULL; dsa_detach(pgStatCustomLocal.dsa); /* * dsa_detach() does not decrement the DSA reference count as no segment * was provided to dsa_attach_in_place(), causing no cleanup callbacks to * be registered. Hence, release it manually now. */ dsa_release_in_place(pgStatCustomLocal.shmem->raw_dsa_area); pgStatCustomLocal.dsa = NULL; } /* ------------------------------------------------------------ * Maintenance of shared memory stats entries * ------------------------------------------------------------ */ /* * Initialize entry newly-created. * * Returns NULL in the event of an allocation failure, so as callers can * take cleanup actions as the entry initialized is already inserted in the * shared hashtable. */ static PgStatShared_Common * pgstat_custom_init_entry(PgStat_Kind kind, PgStatShared_HashEntry *shhashent) { /* Create new stats entry. */ dsa_pointer chunk; PgStatShared_Common *shheader; /* * Initialize refcount to 1, marking it as valid / not dropped. The entry * can't be freed before the initialization because it can't be found as * long as we hold the dshash partition lock. Caller needs to increase * further if a longer lived reference is needed. */ pg_atomic_init_u32(&shhashent->refcount, 1); /* * Initialize "generation" to 0, as freshly created. */ pg_atomic_init_u32(&shhashent->generation, 0); shhashent->dropped = false; chunk = dsa_allocate_extended(pgStatCustomLocal.dsa, pgstat_custom_get_kind_info(kind)->shared_size, DSA_ALLOC_ZERO | DSA_ALLOC_NO_OOM); if (chunk == InvalidDsaPointer) return NULL; shheader = dsa_get_address(pgStatCustomLocal.dsa, chunk); shheader->magic = 0xdeadbeef; /* Link the new entry from the hash entry. */ shhashent->body = chunk; LWLockInitialize(&shheader->lock, LWTRANCHE_PGSTATS_DATA); return shheader; } static PgStatShared_Common * pgstat_custom_reinit_entry(PgStat_Kind kind, PgStatShared_HashEntry *shhashent) { PgStatShared_Common *shheader; shheader = dsa_get_address(pgStatCustomLocal.dsa, shhashent->body); /* mark as not dropped anymore */ pg_atomic_fetch_add_u32(&shhashent->refcount, 1); /* * Increment "generation", to let any backend with local references know * that what they point to is outdated. */ pg_atomic_fetch_add_u32(&shhashent->generation, 1); shhashent->dropped = false; /* reinitialize content */ Assert(shheader->magic == 0xdeadbeef); memset(pgstat_custom_get_entry_data(kind, shheader), 0, pgstat_custom_get_entry_len(kind)); return shheader; } static void pgstat_custom_setup_shared_refs(void) { if (likely(pgStatCustomEntryRefHash != NULL)) return; pgStatCustomEntryRefHash = pgstat_custom_entry_ref_hash_create(pgStatCustomEntryRefHashContext, PGSTAT_ENTRY_REF_HASH_SIZE, NULL); pgStatCustomSharedRefAge = pg_atomic_read_u64(&pgStatCustomLocal.shmem->gc_request_count); Assert(pgStatCustomSharedRefAge != 0); } /* * Helper function for pgstat_get_entry_ref(). */ static void pgstat_custom_acquire_entry_ref(PgStat_EntryRef *entry_ref, PgStatShared_HashEntry *shhashent, PgStatShared_Common *shheader) { Assert(shheader->magic == 0xdeadbeef); Assert(pg_atomic_read_u32(&shhashent->refcount) > 0); pg_atomic_fetch_add_u32(&shhashent->refcount, 1); dshash_release_lock(pgStatCustomLocal.shared_hash, shhashent); entry_ref->shared_stats = shheader; entry_ref->shared_entry = shhashent; entry_ref->generation = pg_atomic_read_u32(&shhashent->generation); } /* * Helper function for pgstat_get_entry_ref(). */ static bool pgstat_custom_get_entry_ref_cached(PgStat_HashKey key, PgStat_EntryRef **entry_ref_p) { bool found; PgStat_EntryRefHashEntry *cache_entry; /* * We immediately insert a cache entry, because it avoids 1) multiple * hashtable lookups in case of a cache miss 2) having to deal with * out-of-memory errors after incrementing PgStatShared_Common->refcount. */ cache_entry = pgstat_custom_entry_ref_hash_insert(pgStatCustomEntryRefHash, key, &found); if (!found || !cache_entry->entry_ref) { PgStat_EntryRef *entry_ref; cache_entry->entry_ref = entry_ref = MemoryContextAlloc(pgStatCustomSharedRefContext, sizeof(PgStat_EntryRef)); entry_ref->shared_stats = NULL; entry_ref->shared_entry = NULL; entry_ref->pending = NULL; found = false; } else if (cache_entry->entry_ref->shared_stats == NULL) { Assert(cache_entry->entry_ref->pending == NULL); found = false; } else { PgStat_EntryRef *entry_ref PG_USED_FOR_ASSERTS_ONLY; entry_ref = cache_entry->entry_ref; Assert(entry_ref->shared_entry != NULL); Assert(entry_ref->shared_stats != NULL); Assert(entry_ref->shared_stats->magic == 0xdeadbeef); /* should have at least our reference */ Assert(pg_atomic_read_u32(&entry_ref->shared_entry->refcount) > 0); } *entry_ref_p = cache_entry->entry_ref; return found; } /* * Get a shared stats reference. If create is true, the shared stats object is * created if it does not exist. * * When create is true, and created_entry is non-NULL, it'll be set to true * if the entry is newly created, false otherwise. */ PgStat_EntryRef * pgstat_custom_get_entry_ref(PgStat_Kind kind, Oid dboid, Oid objid, bool create, bool *created_entry) { PgStat_HashKey key; PgStatShared_HashEntry *shhashent; PgStatShared_Common *shheader = NULL; PgStat_EntryRef *entry_ref; /* clear padding */ memset(&key, 0, sizeof(struct PgStat_HashKey)); key.kind = kind; key.dboid = dboid; key.objoid = objid; /* * passing in created_entry only makes sense if we possibly could create * entry. */ Assert(create || created_entry == NULL); pgstat_custom_assert_is_up(); Assert(pgStatCustomLocal.shared_hash != NULL); Assert(!pgStatCustomLocal.shmem->is_shutdown); pgstat_custom_setup_memcxt(); pgstat_custom_setup_shared_refs(); if (created_entry != NULL) *created_entry = false; /* * Check if other backends dropped stats that could not be deleted because * somebody held references to it. If so, check this backend's references. * This is not expected to happen often. The location of the check is a * bit random, but this is a relatively frequently called path, so better * than most. */ if (pgstat_custom_need_entry_refs_gc()) pgstat_custom_gc_entry_refs(); /* * First check the lookup cache hashtable in local memory. If we find a * match here we can avoid taking locks / causing contention. */ if (pgstat_custom_get_entry_ref_cached(key, &entry_ref)) return entry_ref; Assert(entry_ref != NULL); /* * Do a lookup in the hash table first - it's quite likely that the entry * already exists, and that way we only need a shared lock. */ shhashent = dshash_find(pgStatCustomLocal.shared_hash, &key, false); if (create && !shhashent) { bool shfound; /* * It's possible that somebody created the entry since the above * lookup. If so, fall through to the same path as if we'd have if it * already had been created before the dshash_find() calls. */ shhashent = dshash_find_or_insert(pgStatCustomLocal.shared_hash, &key, &shfound); if (!shfound) { shheader = pgstat_custom_init_entry(kind, shhashent); if (shheader == NULL) { /* * Failed the allocation of a new entry, so clean up the * shared hashtable before giving up. */ dshash_delete_entry(pgStatCustomLocal.shared_hash, shhashent); ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of memory"), errdetail("Failed while allocating entry %u/%u/%u.", key.kind, key.dboid, key.objoid))); } pgstat_custom_acquire_entry_ref(entry_ref, shhashent, shheader); if (created_entry != NULL) *created_entry = true; return entry_ref; } } if (!shhashent) { /* * If we're not creating, delete the reference again. In all * likelihood it's just a stats lookup - no point wasting memory for a * shared ref to nothing... */ pgstat_custom_release_entry_ref(key, entry_ref, false); return NULL; } else { /* * Can get here either because dshash_find() found a match, or if * dshash_find_or_insert() found a concurrently inserted entry. */ if (shhashent->dropped && create) { /* * There are legitimate cases where the old stats entry might not * yet have been dropped by the time it's reused. The most obvious * case are replication slot stats, where a new slot can be * created with the same index just after dropping. But oid * wraparound can lead to other cases as well. We just reset the * stats to their plain state, while incrementing its "generation" * in the shared entry for any remaining local references. */ shheader = pgstat_custom_reinit_entry(kind, shhashent); pgstat_custom_acquire_entry_ref(entry_ref, shhashent, shheader); if (created_entry != NULL) *created_entry = true; return entry_ref; } else if (shhashent->dropped) { dshash_release_lock(pgStatCustomLocal.shared_hash, shhashent); pgstat_custom_release_entry_ref(key, entry_ref, false); return NULL; } else { shheader = dsa_get_address(pgStatCustomLocal.dsa, shhashent->body); pgstat_custom_acquire_entry_ref(entry_ref, shhashent, shheader); return entry_ref; } } } static void pgstat_custom_release_entry_ref(PgStat_HashKey key, PgStat_EntryRef *entry_ref, bool discard_pending) { if (entry_ref && entry_ref->pending) { if (discard_pending) pgstat_custom_delete_pending_entry(entry_ref); else elog(ERROR, "releasing ref with pending data"); } if (entry_ref && entry_ref->shared_stats) { Assert(entry_ref->shared_stats->magic == 0xdeadbeef); Assert(entry_ref->pending == NULL); /* * This can't race with another backend looking up the stats entry and * increasing the refcount because it is not "legal" to create * additional references to dropped entries. */ if (pg_atomic_fetch_sub_u32(&entry_ref->shared_entry->refcount, 1) == 1) { PgStatShared_HashEntry *shent; /* * We're the last referrer to this entry, try to drop the shared * entry. */ /* only dropped entries can reach a 0 refcount */ Assert(entry_ref->shared_entry->dropped); shent = dshash_find(pgStatCustomLocal.shared_hash, &entry_ref->shared_entry->key, true); if (!shent) elog(ERROR, "could not find just referenced shared stats entry"); /* * This entry may have been reinitialized while trying to release * it, so double-check that it has not been reused while holding a * lock on its shared entry. */ if (pg_atomic_read_u32(&entry_ref->shared_entry->generation) == entry_ref->generation) { /* Same "generation", so we're OK with the removal */ Assert(pg_atomic_read_u32(&entry_ref->shared_entry->refcount) == 0); Assert(entry_ref->shared_entry == shent); pgstat_custom_free_entry(shent, NULL); } else { /* * Shared stats entry has been reinitialized, so do not drop * its shared entry, only release its lock. */ dshash_release_lock(pgStatCustomLocal.shared_hash, shent); } } } if (!pgstat_custom_entry_ref_hash_delete(pgStatCustomEntryRefHash, key)) elog(ERROR, "entry ref vanished before deletion"); if (entry_ref) pfree(entry_ref); } /* * Acquire exclusive lock on the entry. * * If nowait is true, it's just a conditional acquire, and the result * *must* be checked to verify success. * If nowait is false, waits as necessary, always returning true. */ bool pgstat_custom_lock_entry(PgStat_EntryRef *entry_ref, bool nowait) { LWLock *lock = &entry_ref->shared_stats->lock; if (nowait) return LWLockConditionalAcquire(lock, LW_EXCLUSIVE); LWLockAcquire(lock, LW_EXCLUSIVE); return true; } /* * Acquire shared lock on the entry. * * Separate from pgstat_lock_entry() as most callers will need to lock * exclusively. The wait semantics are identical. */ bool pgstat_custom_lock_entry_shared(PgStat_EntryRef *entry_ref, bool nowait) { LWLock *lock = &entry_ref->shared_stats->lock; if (nowait) return LWLockConditionalAcquire(lock, LW_SHARED); LWLockAcquire(lock, LW_SHARED); return true; } void pgstat_custom_unlock_entry(PgStat_EntryRef *entry_ref) { LWLockRelease(&entry_ref->shared_stats->lock); } void pgstat_custom_request_entry_refs_gc(void) { pg_atomic_fetch_add_u64(&pgStatCustomLocal.shmem->gc_request_count, 1); } static bool pgstat_custom_need_entry_refs_gc(void) { uint64 curage; if (!pgStatCustomEntryRefHash) return false; /* should have been initialized when creating pgStatCustomEntryRefHash */ Assert(pgStatCustomSharedRefAge != 0); curage = pg_atomic_read_u64(&pgStatCustomLocal.shmem->gc_request_count); return pgStatCustomSharedRefAge != curage; } static void pgstat_custom_gc_entry_refs(void) { pgstat_custom_entry_ref_hash_iterator i; PgStat_EntryRefHashEntry *ent; uint64 curage; curage = pg_atomic_read_u64(&pgStatCustomLocal.shmem->gc_request_count); Assert(curage != 0); /* * Some entries have been dropped or reinitialized. Invalidate cache * pointer to them. */ pgstat_custom_entry_ref_hash_start_iterate(pgStatCustomEntryRefHash, &i); while ((ent = pgstat_custom_entry_ref_hash_iterate(pgStatCustomEntryRefHash, &i)) != NULL) { PgStat_EntryRef *entry_ref = ent->entry_ref; Assert(!entry_ref->shared_stats || entry_ref->shared_stats->magic == 0xdeadbeef); /* * "generation" checks for the case of entries being reinitialized, * and "dropped" for the case where these are.. dropped. */ if (!entry_ref->shared_entry->dropped && pg_atomic_read_u32(&entry_ref->shared_entry->generation) == entry_ref->generation) continue; /* cannot gc shared ref that has pending data */ if (entry_ref->pending != NULL) continue; pgstat_custom_release_entry_ref(ent->key, entry_ref, false); } pgStatCustomSharedRefAge = curage; } static void pgstat_custom_release_matching_entry_refs(bool discard_pending, ReleaseMatchCB match, Datum match_data) { pgstat_custom_entry_ref_hash_iterator i; PgStat_EntryRefHashEntry *ent; if (pgStatCustomEntryRefHash == NULL) return; pgstat_custom_entry_ref_hash_start_iterate(pgStatCustomEntryRefHash, &i); while ((ent = pgstat_custom_entry_ref_hash_iterate(pgStatCustomEntryRefHash, &i)) != NULL) { Assert(ent->entry_ref != NULL); if (match && !match(ent, match_data)) continue; pgstat_custom_release_entry_ref(ent->key, ent->entry_ref, discard_pending); } } /* * Release all local references to shared stats entries. * * When a process exits it cannot do so while still holding references onto * stats entries, otherwise the shared stats entries could never be freed. */ static void pgstat_custom_release_all_entry_refs(bool discard_pending) { if (pgStatCustomEntryRefHash == NULL) return; pgstat_custom_release_matching_entry_refs(discard_pending, NULL, 0); Assert(pgStatCustomEntryRefHash->members == 0); pgstat_custom_entry_ref_hash_destroy(pgStatCustomEntryRefHash); pgStatCustomEntryRefHash = NULL; } static bool match_db(PgStat_EntryRefHashEntry *ent, Datum match_data) { Oid dboid = DatumGetObjectId(match_data); return ent->key.dboid == dboid; } static void pgstat_custom_release_db_entry_refs(Oid dboid) { pgstat_custom_release_matching_entry_refs( /* discard pending = */ true, match_db, ObjectIdGetDatum(dboid)); } /* * Helper for both pgstat_drop_database_and_contents() and * pgstat_drop_entry(). If hstat is non-null delete the shared entry using * dshash_delete_current(), otherwise use dshash_delete_entry(). In either * case the entry needs to be already locked. */ static bool pgstat_custom_drop_entry_internal(PgStatShared_HashEntry *shent, dshash_seq_status *hstat) { Assert(shent->body != InvalidDsaPointer); /* should already have released local reference */ if (pgStatCustomEntryRefHash) Assert(!pgstat_custom_entry_ref_hash_lookup(pgStatCustomEntryRefHash, shent->key)); /* * Signal that the entry is dropped - this will eventually cause other * backends to release their references. */ if (shent->dropped) elog(ERROR, "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%u refcount=%u generation=%u", pgstat_get_kind_info(shent->key.kind)->name, shent->key.dboid, shent->key.objoid, pg_atomic_read_u32(&shent->refcount), pg_atomic_read_u32(&shent->generation)); shent->dropped = true; /* release refcount marking entry as not dropped */ if (pg_atomic_sub_fetch_u32(&shent->refcount, 1) == 0) { pgstat_custom_free_entry(shent, hstat); return true; } else { if (!hstat) dshash_release_lock(pgStatCustomLocal.shared_hash, shent); return false; } } /* * Drop stats for the database and all the objects inside that database. */ static void pgstat_custom_drop_database_and_contents(Oid dboid) { dshash_seq_status hstat; PgStatShared_HashEntry *p; uint64 not_freed_count = 0; Assert(OidIsValid(dboid)); Assert(pgStatCustomLocal.shared_hash != NULL); /* * This backend might very well be the only backend holding a reference to * about-to-be-dropped entries. Ensure that we're not preventing it from * being cleaned up till later. * * Doing this separately from the dshash iteration below avoids having to * do so while holding a partition lock on the shared hashtable. */ pgstat_custom_release_db_entry_refs(dboid); /* some of the dshash entries are to be removed, take exclusive lock. */ dshash_seq_init(&hstat, pgStatCustomLocal.shared_hash, true); while ((p = dshash_seq_next(&hstat)) != NULL) { if (p->dropped) continue; if (p->key.dboid != dboid) continue; if (!pgstat_custom_drop_entry_internal(p, &hstat)) { /* * Even statistics for a dropped database might currently be * accessed (consider e.g. database stats for pg_stat_database). */ not_freed_count++; } } dshash_seq_term(&hstat); /* * If some of the stats data could not be freed, signal the reference * holders to run garbage collection of their cached pgStatShmLookupCache. */ if (not_freed_count > 0) pgstat_request_entry_refs_gc(); } /* * Drop a single stats entry. * * This routine returns false if the stats entry of the dropped object could * not be freed, true otherwise. * * The callers of this function should call pgstat_request_entry_refs_gc() * if the stats entry could not be freed, to ensure that this entry's memory * can be reclaimed later by a different backend calling * pgstat_custom_gc_entry_refs(). */ bool pgstat_custom_drop_entry(PgStat_Kind kind, Oid dboid, Oid objid) { PgStat_HashKey key; PgStatShared_HashEntry *shent; bool freed = true; /* clear padding */ memset(&key, 0, sizeof(struct PgStat_HashKey)); key.kind = kind; key.dboid = dboid; key.objoid = objid; /* delete local reference */ if (pgStatCustomEntryRefHash) { PgStat_EntryRefHashEntry *lohashent = pgstat_custom_entry_ref_hash_lookup(pgStatCustomEntryRefHash, key); if (lohashent) pgstat_custom_release_entry_ref(lohashent->key, lohashent->entry_ref, true); } /* mark entry in shared hashtable as deleted, drop if possible */ shent = dshash_find(pgStatCustomLocal.shared_hash, &key, true); if (shent) { freed = pgstat_custom_drop_entry_internal(shent, NULL); /* * Database stats contain other stats. Drop those as well when * dropping the database. XXX: Perhaps this should be done in a * slightly more principled way? But not obvious what that'd look * like, and so far this is the only case... */ if (key.kind == PGSTAT_KIND_DATABASE) pgstat_custom_drop_database_and_contents(key.dboid); } return freed; } /* * Scan through the shared hashtable of stats, dropping statistics if * approved by the optional do_drop() function. */ void pgstat_custom_drop_matching_entries(bool (*do_drop) (PgStatShared_HashEntry *, Datum), Datum match_data) { dshash_seq_status hstat; PgStatShared_HashEntry *ps; uint64 not_freed_count = 0; /* entries are removed, take an exclusive lock */ dshash_seq_init(&hstat, pgStatCustomLocal.shared_hash, true); while ((ps = dshash_seq_next(&hstat)) != NULL) { if (ps->dropped) continue; if (do_drop != NULL && !do_drop(ps, match_data)) continue; /* delete local reference */ if (pgStatCustomEntryRefHash) { PgStat_EntryRefHashEntry *lohashent = pgstat_custom_entry_ref_hash_lookup(pgStatCustomEntryRefHash, ps->key); if (lohashent) pgstat_custom_release_entry_ref(lohashent->key, lohashent->entry_ref, true); } if (!pgstat_custom_drop_entry_internal(ps, &hstat)) not_freed_count++; } dshash_seq_term(&hstat); if (not_freed_count > 0) pgstat_custom_request_entry_refs_gc(); } /* ------------------------------------------------------------ * Dropping and resetting of stats entries * ------------------------------------------------------------ */ static void pgstat_custom_free_entry(PgStatShared_HashEntry *shent, dshash_seq_status *hstat) { dsa_pointer pdsa; /* * Fetch dsa pointer before deleting entry - that way we can free the * memory after releasing the lock. */ pdsa = shent->body; if (!hstat) dshash_delete_entry(pgStatCustomLocal.shared_hash, shent); else dshash_delete_current(hstat); dsa_free(pgStatCustomLocal.dsa, pdsa); } static void pgstat_custom_setup_memcxt(void) { if (unlikely(!pgStatCustomSharedRefContext)) pgStatCustomSharedRefContext = AllocSetContextCreate(TopMemoryContext, "PgStat Custom Shared Ref", ALLOCSET_SMALL_SIZES); if (unlikely(!pgStatCustomEntryRefHashContext)) pgStatCustomEntryRefHashContext = AllocSetContextCreate(TopMemoryContext, "PgStat Custom Shared Ref Hash", ALLOCSET_SMALL_SIZES); } #endif pganalyze-pg_stat_plans-e82aba9/compat_16_17/pgstat_custom.h000066400000000000000000000060471521051134300242040ustar00rootroot00000000000000#ifndef PGSTAT_CUSTOM_H #define PGSTAT_CUSTOM_H #include "utils/pgstat_internal.h" #if PG_VERSION_NUM >= 180000 /* Alias built-in stats functions, since we can rely on them to support custom stats */ #define pgStatCustomLocal pgStatLocal #define pgstat_custom_lock_entry pgstat_lock_entry #define pgstat_custom_unlock_entry pgstat_unlock_entry #define pgstat_custom_drop_entry pgstat_drop_entry #define pgstat_custom_drop_matching_entries pgstat_drop_matching_entries #define pgstat_custom_request_entry_refs_gc pgstat_request_entry_refs_gc #define pgstat_custom_get_entry_data pgstat_get_entry_data #define pgstat_custom_get_kind_info pgstat_get_kind_info #define pgstat_custom_register_kind pgstat_register_kind #define pgstat_custom_prep_pending_entry pgstat_prep_pending_entry #define pgstat_custom_drop_matching_entries pgstat_drop_matching_entries #else extern PGDLLIMPORT PgStat_LocalState pgStatCustomLocal; #ifdef USE_ASSERT_CHECKING extern void pgstat_custom_assert_is_up(void); #else #define pgstat_custom_assert_is_up() ((void)true) #endif extern void pgstat_custom_initialize(void); extern void StatsCustomShmemInit(void); extern void pgstat_custom_attach_shmem(void); extern void pgstat_custom_detach_shmem(void); extern long pgstat_custom_report_stat(bool force); extern const PgStat_KindInfo *pgstat_custom_get_kind_info(PgStat_Kind kind); extern void pgstat_custom_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info); extern void pgstat_custom_delete_pending_entry(PgStat_EntryRef *entry_ref); extern PgStat_EntryRef *pgstat_custom_prep_pending_entry(PgStat_Kind kind, Oid dboid, Oid objid, bool *created_entry); extern PgStat_EntryRef *pgstat_custom_fetch_pending_entry(PgStat_Kind kind, Oid dboid, Oid objid); extern PgStat_EntryRef * pgstat_custom_get_entry_ref(PgStat_Kind kind, Oid dboid, Oid objid, bool create, bool *created_entry); extern bool pgstat_custom_lock_entry(PgStat_EntryRef *entry_ref, bool nowait); extern bool pgstat_custom_lock_entry_shared(PgStat_EntryRef *entry_ref, bool nowait); extern void pgstat_custom_unlock_entry(PgStat_EntryRef *entry_ref); extern bool pgstat_custom_drop_entry(PgStat_Kind kind, Oid dboid, Oid objoid); extern void pgstat_custom_drop_matching_entries(bool (*do_drop) (PgStatShared_HashEntry *, Datum), Datum match_data); extern void pgstat_custom_request_entry_refs_gc(void); static inline void *pgstat_custom_get_entry_data(PgStat_Kind kind, PgStatShared_Common *entry); /* * The length of the data portion of a shared memory stats entry (i.e. without * transient data such as refcounts, lwlocks, ...). */ static inline size_t pgstat_custom_get_entry_len(PgStat_Kind kind) { return pgstat_custom_get_kind_info(kind)->shared_data_len; } /* * Returns a pointer to the data portion of a shared memory stats entry. */ static inline void * pgstat_custom_get_entry_data(PgStat_Kind kind, PgStatShared_Common *entry) { size_t off = pgstat_custom_get_kind_info(kind)->shared_data_off; Assert(off != 0 && off < PG_UINT32_MAX); return ((char *) (entry)) + off; } #endif #endif /* PGSTAT_CUSTOM_H */ pganalyze-pg_stat_plans-e82aba9/compat_18/000077500000000000000000000000001521051134300206235ustar00rootroot00000000000000pganalyze-pg_stat_plans-e82aba9/compat_18/expected/000077500000000000000000000000001521051134300224245ustar00rootroot00000000000000pganalyze-pg_stat_plans-e82aba9/compat_18/expected/activity.out000066400000000000000000000046611521051134300250200ustar00rootroot00000000000000SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- check if we see our own plan in activity -- SELECT plan FROM pg_stat_plans_activity WHERE pid = pg_backend_pid(); plan ----------------------------------------------------------------------------------------------------------------------- Hash Join + Hash Cond: ((p.dbid = a.datid) AND (p.userid = a.usesysid) AND (p.queryid = a.query_id) AND (p.planid = a.plan_id))+ -> Function Scan on pg_stat_plans p + Filter: (toplevel IS TRUE) + -> Hash + -> Function Scan on pg_stat_plans_get_activity a + Filter: (pid = pg_backend_pid()) (1 row) -- -- check if we handle showing our plan for named prepared statements correctly -- PREPARE x AS SELECT plan FROM pg_stat_plans_activity WHERE pid = pg_backend_pid(); EXECUTE x; plan ----------------------------------------------------------------------------------------------------------------------- Hash Join + Hash Cond: ((p.dbid = a.datid) AND (p.userid = a.usesysid) AND (p.queryid = a.query_id) AND (p.planid = a.plan_id))+ -> Function Scan on pg_stat_plans p + Filter: (toplevel IS TRUE) + -> Hash + -> Function Scan on pg_stat_plans_get_activity a + Filter: (pid = pg_backend_pid()) (1 row) DEALLOCATE x; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/compat_18/expected/cleanup.out000066400000000000000000000000361521051134300246030ustar00rootroot00000000000000DROP EXTENSION pg_stat_plans; pganalyze-pg_stat_plans-e82aba9/compat_18/expected/privileges.out000066400000000000000000000101221521051134300253220ustar00rootroot00000000000000-- -- Only superusers and roles with privileges of the pg_read_all_stats role -- are allowed to see the plan text, queryid and planid of queries executed by -- other users. Other users can see the statistics. -- CREATE ROLE regress_stats_superuser SUPERUSER; CREATE ROLE regress_stats_user1; CREATE ROLE regress_stats_user2; GRANT pg_read_all_stats TO regress_stats_user2; SET ROLE regress_stats_superuser; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) SELECT 1 AS "SUPER"; SUPER ------- 1 (1 row) SET ROLE regress_stats_user1; SELECT 1+1 AS "ONE"; ONE ----- 2 (1 row) SET ROLE regress_stats_user2; SELECT 1+1 AS "TWO"; TWO ----- 2 (1 row) -- Wait for pending stats to be flushed SET ROLE regress_stats_superuser; SELECT pg_sleep(1); pg_sleep ---------- (1 row) -- -- regress_stats_user1 has no privileges to read the plan text, queryid -- or planid of queries executed by others but can see statistics -- like calls and rows. -- -- We run this before the other tests so that we don't have a surprising -- "" entry from other roles checking pg_stat_plans. -- SET ROLE regress_stats_user1; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------------------------+------- regress_stats_superuser | | | | 1 regress_stats_superuser | | | | 1 regress_stats_superuser | | | | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | | | | 1 (5 rows) -- -- A superuser can read all columns of queries executed by others, -- including plan text, queryid and planid. -- SET ROLE regress_stats_superuser; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------+------- regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | t | t | Result | 1 (5 rows) -- -- regress_stats_user2, with pg_read_all_stats role privileges, can -- read all columns, including plan text, queryid and planid, of queries -- executed by others. -- SET ROLE regress_stats_user2; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------+------- regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | t | t | Result | 1 (5 rows) -- -- cleanup -- RESET ROLE; DROP ROLE regress_stats_superuser; DROP ROLE regress_stats_user1; DROP ROLE regress_stats_user2; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/compat_18/expected/select.out000066400000000000000000000273561521051134300244510ustar00rootroot00000000000000-- -- SELECT statements -- CREATE EXTENSION pg_stat_plans; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- simple statements -- SELECT 1 FROM pg_class LIMIT 1; ?column? ---------- 1 (1 row) SELECT 1 FROM pg_class WHERE relname = 'pg_class'; ?column? ---------- 1 (1 row) SET enable_indexscan = off; SELECT 1 FROM pg_class WHERE relname = 'pg_class'; ?column? ---------- 1 (1 row) SET enable_indexscan = on; -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls --------------------------------------------------------------+------- Bitmap Heap Scan on pg_class +| 1 Recheck Cond: (relname = 'pg_class'::name) +| -> Bitmap Index Scan on pg_class_relname_nsp_index +| Index Cond: (relname = 'pg_class'::name) | Index Only Scan using pg_class_relname_nsp_index on pg_class+| 1 Index Cond: (relname = 'pg_class'::name) | Limit +| 1 -> Seq Scan on pg_class | Result | 1 Result | 1 Sort +| 0 Sort Key: pg_stat_plans.plan COLLATE "C" +| -> Function Scan on pg_stat_plans | (6 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- subplans and CTEs -- WITH x AS MATERIALIZED (SELECT 1) SELECT * FROM x; ?column? ---------- 1 (1 row) SELECT a.attname, (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) FROM pg_catalog.pg_attribute a WHERE a.attrelid = 'pg_class'::regclass ORDER BY attnum LIMIT 1; attname | pg_get_expr ----------+------------- tableoid | (1 row) -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls -------------------------------------------------------------------------------+------- CTE Scan on x +| 1 CTE x +| -> Result | Limit +| 1 -> Index Scan using pg_attribute_relid_attnum_index on pg_attribute a +| Index Cond: (attrelid = '1259'::oid) +| SubPlan 1 +| -> Result +| One-Time Filter: a.atthasdef +| -> Seq Scan on pg_attrdef d +| Filter: ((adrelid = a.attrelid) AND (adnum = a.attnum)) | Result | 1 Result | 1 Sort +| 0 Sort Key: pg_stat_plans.plan COLLATE "C" +| -> Function Scan on pg_stat_plans | (5 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- partitoning -- create table lp (a char) partition by list (a); create table lp_default partition of lp default; create table lp_ef partition of lp for values in ('e', 'f'); create table lp_ad partition of lp for values in ('a', 'd'); create table lp_bc partition of lp for values in ('b', 'c'); create table lp_g partition of lp for values in ('g'); create table lp_null partition of lp for values in (null); select * from lp; a --- (0 rows) select * from lp where a > 'a' and a < 'd'; a --- (0 rows) select * from lp where a > 'a' and a <= 'd'; a --- (0 rows) select * from lp where a = 'a'; a --- (0 rows) select * from lp where 'a' = a; /* commuted */ a --- (0 rows) select * from lp where a is not null; a --- (0 rows) select * from lp where a is null; a --- (0 rows) select * from lp where a = 'a' or a = 'c'; a --- (0 rows) select * from lp where a is not null and (a = 'a' or a = 'c'); a --- (0 rows) select * from lp where a <> 'g'; a --- (0 rows) select * from lp where a <> 'a' and a <> 'd'; a --- (0 rows) select * from lp where a not in ('a', 'd'); a --- (0 rows) -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls --------------------------------------------------------------------------------+------- Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar)) +| -> Seq Scan on lp_bc lp_2 +| Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar)) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) +| -> Seq Scan on lp_bc lp_2 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) +| -> Seq Scan on lp_default lp_3 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar)))+| -> Seq Scan on lp_bc lp_2 +| Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar))) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_bc lp_2 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_ef lp_3 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_default lp_4 +| Filter: (a <> 'g'::bpchar) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_bc lp_2 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_ef lp_3 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_g lp_4 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_default lp_5 +| Filter: (a IS NOT NULL) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| -> Seq Scan on lp_bc lp_2 +| -> Seq Scan on lp_ef lp_3 +| -> Seq Scan on lp_g lp_4 +| -> Seq Scan on lp_null lp_5 +| -> Seq Scan on lp_default lp_6 | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_ef lp_2 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_g lp_3 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_default lp_4 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: ((a > 'a'::bpchar) AND (a < 'd'::bpchar)) +| -> Seq Scan on lp_default lp_2 +| Filter: ((a > 'a'::bpchar) AND (a < 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_ef lp_2 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_g lp_3 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_default lp_4 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) | Result | 1 Result | 1 Seq Scan on lp_ad lp +| 1 Filter: ('a'::bpchar = a) | Seq Scan on lp_ad lp +| 1 Filter: (a = 'a'::bpchar) | Seq Scan on lp_null lp +| 1 Filter: (a IS NULL) | Sort +| 0 Sort Key: pg_stat_plans.plan COLLATE "C" +| -> Function Scan on pg_stat_plans | (15 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/expected/000077500000000000000000000000001521051134300206315ustar00rootroot00000000000000pganalyze-pg_stat_plans-e82aba9/expected/activity.out000066400000000000000000000046611521051134300232250ustar00rootroot00000000000000SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- check if we see our own plan in activity -- SELECT plan FROM pg_stat_plans_activity WHERE pid = pg_backend_pid(); plan ----------------------------------------------------------------------------------------------------------------------- Hash Join + Hash Cond: ((p.dbid = a.datid) AND (p.userid = a.usesysid) AND (p.queryid = a.query_id) AND (p.planid = a.plan_id))+ -> Function Scan on pg_stat_plans p + Filter: (toplevel IS TRUE) + -> Hash + -> Function Scan on pg_stat_plans_get_activity a + Filter: (pid = pg_backend_pid()) (1 row) -- -- check if we handle showing our plan for named prepared statements correctly -- PREPARE x AS SELECT plan FROM pg_stat_plans_activity WHERE pid = pg_backend_pid(); EXECUTE x; plan ----------------------------------------------------------------------------------------------------------------------- Hash Join + Hash Cond: ((p.dbid = a.datid) AND (p.userid = a.usesysid) AND (p.queryid = a.query_id) AND (p.planid = a.plan_id))+ -> Function Scan on pg_stat_plans p + Filter: (toplevel IS TRUE) + -> Hash + -> Function Scan on pg_stat_plans_get_activity a + Filter: (pid = pg_backend_pid()) (1 row) DEALLOCATE x; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/expected/cleanup.out000066400000000000000000000000361521051134300230100ustar00rootroot00000000000000DROP EXTENSION pg_stat_plans; pganalyze-pg_stat_plans-e82aba9/expected/plan_advice.out000066400000000000000000000031451521051134300236320ustar00rootroot00000000000000-- -- pg_plan_advice string collection (Postgres 19+ only) -- -- This test is only run when pg_plan_advice is available and preloaded -- alongside pg_stat_plans (see the Makefile). The extension was already -- created by the "select" test. -- -- Disable parallelism so the chosen plans (and thus the generated advice) -- are deterministic. SET max_parallel_workers_per_gather = 0; CREATE TABLE pa_t1(id int primary key, x int); CREATE TABLE pa_t2(id int primary key, t1_id int); INSERT INTO pa_t1 SELECT g, g FROM generate_series(1, 1000) g; INSERT INTO pa_t2 SELECT g, g % 1000 + 1 FROM generate_series(1, 5000) g; ANALYZE pa_t1, pa_t2; -- -- off: advice is never collected, even on repeated execution -- SET pg_stat_plans.plan_advice = off; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) SELECT count(*) FROM pa_t1 WHERE x < 100; count ------- 99 (1 row) SELECT count(*) FROM pa_t1 WHERE x < 100; count ------- 99 (1 row) SELECT plan_advice IS NULL AS advice_is_null FROM pg_stat_plans WHERE plan LIKE '%pa_t1%' AND plan NOT LIKE '%pg_stat_plans%'; advice_is_null ---------------- t (1 row) -- -- on: advice is collected on the first execution -- SET pg_stat_plans.plan_advice = on; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) SELECT count(*) FROM pa_t1 WHERE x < 100; count ------- 99 (1 row) SELECT plan_advice IS NOT NULL AS advice_present FROM pg_stat_plans WHERE plan LIKE '%pa_t1%' AND plan NOT LIKE '%pg_stat_plans%'; advice_present ---------------- t (1 row) DROP TABLE pa_t1, pa_t2; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/expected/privileges.out000066400000000000000000000101221521051134300235270ustar00rootroot00000000000000-- -- Only superusers and roles with privileges of the pg_read_all_stats role -- are allowed to see the plan text, queryid and planid of queries executed by -- other users. Other users can see the statistics. -- CREATE ROLE regress_stats_superuser SUPERUSER; CREATE ROLE regress_stats_user1; CREATE ROLE regress_stats_user2; GRANT pg_read_all_stats TO regress_stats_user2; SET ROLE regress_stats_superuser; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) SELECT 1 AS "SUPER"; SUPER ------- 1 (1 row) SET ROLE regress_stats_user1; SELECT 1+1 AS "ONE"; ONE ----- 2 (1 row) SET ROLE regress_stats_user2; SELECT 1+1 AS "TWO"; TWO ----- 2 (1 row) -- Wait for pending stats to be flushed SET ROLE regress_stats_superuser; SELECT pg_sleep(1); pg_sleep ---------- (1 row) -- -- regress_stats_user1 has no privileges to read the plan text, queryid -- or planid of queries executed by others but can see statistics -- like calls and rows. -- -- We run this before the other tests so that we don't have a surprising -- "" entry from other roles checking pg_stat_plans. -- SET ROLE regress_stats_user1; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------------------------+------- regress_stats_superuser | | | | 1 regress_stats_superuser | | | | 1 regress_stats_superuser | | | | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | | | | 1 (5 rows) -- -- A superuser can read all columns of queries executed by others, -- including plan text, queryid and planid. -- SET ROLE regress_stats_superuser; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------+------- regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | t | t | Result | 1 (5 rows) -- -- regress_stats_user2, with pg_read_all_stats role privileges, can -- read all columns, including plan text, queryid and planid, of queries -- executed by others. -- SET ROLE regress_stats_user2; SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.planid <> 0 AS planid_bool, ss.plan, ss.calls FROM pg_stat_plans ss JOIN pg_roles r ON ss.userid = r.oid WHERE ss.plan NOT LIKE '%Function Scan on pg_stat_plans%' ORDER BY r.rolname, ss.plan COLLATE "C", ss.calls; rolname | queryid_bool | planid_bool | plan | calls -------------------------+--------------+-------------+--------+------- regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_superuser | t | t | Result | 1 regress_stats_user1 | t | t | Result | 1 regress_stats_user2 | t | t | Result | 1 (5 rows) -- -- cleanup -- RESET ROLE; DROP ROLE regress_stats_superuser; DROP ROLE regress_stats_user1; DROP ROLE regress_stats_user2; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/expected/select.out000066400000000000000000000273561521051134300226560ustar00rootroot00000000000000-- -- SELECT statements -- CREATE EXTENSION pg_stat_plans; SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- simple statements -- SELECT 1 FROM pg_class LIMIT 1; ?column? ---------- 1 (1 row) SELECT 1 FROM pg_class WHERE relname = 'pg_class'; ?column? ---------- 1 (1 row) SET enable_indexscan = off; SELECT 1 FROM pg_class WHERE relname = 'pg_class'; ?column? ---------- 1 (1 row) SET enable_indexscan = on; -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls --------------------------------------------------------------+------- Bitmap Heap Scan on pg_class +| 1 Recheck Cond: (relname = 'pg_class'::name) +| -> Bitmap Index Scan on pg_class_relname_nsp_index +| Index Cond: (relname = 'pg_class'::name) | Index Only Scan using pg_class_relname_nsp_index on pg_class+| 1 Index Cond: (relname = 'pg_class'::name) | Limit +| 1 -> Seq Scan on pg_class | Result | 1 Result | 1 Sort +| 0 Sort Key: pg_stat_plans.plan COLLATE "C" +| -> Function Scan on pg_stat_plans | (6 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- subplans and CTEs -- WITH x AS MATERIALIZED (SELECT 1) SELECT * FROM x; ?column? ---------- 1 (1 row) SELECT a.attname, (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) FROM pg_catalog.pg_attribute a WHERE a.attrelid = 'pg_class'::regclass ORDER BY attnum LIMIT 1; attname | pg_get_expr ----------+------------- tableoid | (1 row) -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls -------------------------------------------------------------------------------+------- CTE Scan on x +| 1 CTE x +| -> Result | Limit +| 1 -> Index Scan using pg_attribute_relid_attnum_index on pg_attribute a +| Index Cond: (attrelid = '1259'::oid) +| SubPlan expr_1 +| -> Result +| One-Time Filter: a.atthasdef +| -> Seq Scan on pg_attrdef d +| Filter: ((adrelid = a.attrelid) AND (adnum = a.attnum)) | Result | 1 Result | 1 Sort +| 0 Sort Key: pg_stat_plans.plan COLLATE "C" +| -> Function Scan on pg_stat_plans | (5 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) -- -- partitoning -- create table lp (a char) partition by list (a); create table lp_default partition of lp default; create table lp_ef partition of lp for values in ('e', 'f'); create table lp_ad partition of lp for values in ('a', 'd'); create table lp_bc partition of lp for values in ('b', 'c'); create table lp_g partition of lp for values in ('g'); create table lp_null partition of lp for values in (null); select * from lp; a --- (0 rows) select * from lp where a > 'a' and a < 'd'; a --- (0 rows) select * from lp where a > 'a' and a <= 'd'; a --- (0 rows) select * from lp where a = 'a'; a --- (0 rows) select * from lp where 'a' = a; /* commuted */ a --- (0 rows) select * from lp where a is not null; a --- (0 rows) select * from lp where a is null; a --- (0 rows) select * from lp where a = 'a' or a = 'c'; a --- (0 rows) select * from lp where a is not null and (a = 'a' or a = 'c'); a --- (0 rows) select * from lp where a <> 'g'; a --- (0 rows) select * from lp where a <> 'a' and a <> 'd'; a --- (0 rows) select * from lp where a not in ('a', 'd'); a --- (0 rows) -- Wait for pending stats to be flushed SELECT pg_sleep(1); pg_sleep ---------- (1 row) SELECT plan, calls FROM pg_stat_plans ORDER BY plan COLLATE "C"; plan | calls --------------------------------------------------------------------------------+------- Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar)) +| -> Seq Scan on lp_bc lp_2 +| Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar)) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) +| -> Seq Scan on lp_bc lp_2 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) +| -> Seq Scan on lp_default lp_3 +| Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar)))+| -> Seq Scan on lp_bc lp_2 +| Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar))) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_bc lp_2 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_ef lp_3 +| Filter: (a <> 'g'::bpchar) +| -> Seq Scan on lp_default lp_4 +| Filter: (a <> 'g'::bpchar) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_bc lp_2 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_ef lp_3 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_g lp_4 +| Filter: (a IS NOT NULL) +| -> Seq Scan on lp_default lp_5 +| Filter: (a IS NOT NULL) | Append +| 1 -> Seq Scan on lp_ad lp_1 +| -> Seq Scan on lp_bc lp_2 +| -> Seq Scan on lp_ef lp_3 +| -> Seq Scan on lp_g lp_4 +| -> Seq Scan on lp_null lp_5 +| -> Seq Scan on lp_default lp_6 | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_ef lp_2 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_g lp_3 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) +| -> Seq Scan on lp_default lp_4 +| Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: ((a > 'a'::bpchar) AND (a < 'd'::bpchar)) +| -> Seq Scan on lp_default lp_2 +| Filter: ((a > 'a'::bpchar) AND (a < 'd'::bpchar)) | Append +| 1 -> Seq Scan on lp_bc lp_1 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_ef lp_2 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_g lp_3 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) +| -> Seq Scan on lp_default lp_4 +| Filter: (a <> ALL ('{a,d}'::bpchar[])) | Result | 1 Result | 1 Seq Scan on lp_ad lp +| 1 Filter: ('a'::bpchar = a) | Seq Scan on lp_ad lp +| 1 Filter: (a = 'a'::bpchar) | Seq Scan on lp_null lp +| 1 Filter: (a IS NULL) | Sort +| 0 Sort Key: pg_stat_plans.plan COLLATE "C" +| -> Function Scan on pg_stat_plans | (15 rows) SELECT pg_stat_plans_reset() IS NOT NULL AS t; t --- t (1 row) pganalyze-pg_stat_plans-e82aba9/generate_jumblefuncs.sh000077500000000000000000000047631521051134300235700ustar00rootroot00000000000000#!/usr/bin/env bash # # generate_jumblefuncs.sh # # Regenerate the pgNN_jumblefuncs.{switch,funcs}.c files that jumblefuncs.c # #includes, from a PostgreSQL source tree. # # These files are the queryjumblefuncs.{switch,funcs}.c output of PostgreSQL's # src/backend/nodes/gen_node_support.pl, renamed per major version. The source # tree must be checked out to a branch that carries the pg_stat_plans plan-ID # jumble changes (the "Add plan ID jumble funcs" commit: array_size/JUMBLE_ARRAY # support in gen_node_support.pl plus the plan-node annotations in # plannodes.h / primnodes.h). # # The generator runs standalone with perl -- no configure/build of PostgreSQL # is required, it just parses the node header files. # # Usage: # ./generate_jumblefuncs.sh pg19 ~/Code/postgres # ./generate_jumblefuncs.sh pg18 /path/to/pg18-src # # The header list and its required ordering are read from the target tree's own # gen_node_support.pl (@all_input_files), so this works across major versions. set -euo pipefail if [ "$#" -ne 2 ]; then echo "usage: $0