pax_global_header00006660000000000000000000000064135341255170014520gustar00rootroot0000000000000052 comment=a4ad3d99c7d8cfe3acddbcce00a3d665d3f778fc pg_qualstats-1.0.9/000077500000000000000000000000001353412551700142365ustar00rootroot00000000000000pg_qualstats-1.0.9/.gitignore000066400000000000000000000000171353412551700162240ustar00rootroot00000000000000*.o *.so *.zip pg_qualstats-1.0.9/CHANGELOG000066400000000000000000000103751353412551700154560ustar00rootroot000000000000001.0.9 Bugfix: - Fix constant value truncation when multibyte encoding characters are used (thanks to Gürkan Gür for the report) Miscellaneous: - Remove unneeded cast, that prevented compilation at least on Solaris 10 SPARC (thanks to github user edechaux for the report) 1.0.8 Miscellaneous: - Fix pg12 compatibility - Fix possible issue with array processing 1.0.7 Bugfix: - Fix a bug for queries having JOIN or WHERE predicates on foreign tables or custom scans (Julien Rouhaud). Thanks a lot to Andrej Urvantsev, Raymond Barbiero and mbroxson who all reported this issue, and especially to mbroxson who provided a reproducer! Miscellaneous: - Fix debian packaging to ignore debian/* tags (Christoph Berg) 1.0.6 Bugfix: - Fix bug for handling of nodes having multiple children, such as Append node (Julien Rouhaud). Miscellaneous: - Fix compilation issue with C90 compatibility (Julien Rouhaud) - Fix README.d installation in debian packaging (Thanks to Andreas Beckmann for the report) 1.0.5: Incompatibilites: - Due to changes in pg_stat_statements in 11, queryid is now on 64 bits. SQL functions no longer use oid type but bigint for queryid attribute (even for PG prior to 11). Miscellaneous: - Add pg 11 compatibility (Adrien Nayrat helped by Julien Rouhaud and Thomas Reiss) - Warn if incorrect configuration setting is used 1.0.4: - Fix a bug in Bitmap Index Scan nodes handling for PostgreSQL 10+ (Fix by Julien Rouhaud, thanks to Marc Cousin and Adrien Nayrat for reporting the issue) - Fix sampled array buffer overflow (Fix by Julien Rouhaud, reporting and much testing by Nicolas Gollet) 1.0.3: Bugfix: - Fix a missing call to InstrEndLoop (Tomas Vondra) - Sample all nested queries when top level statement is sampled (Julien Rouhaud) - Make sure hash keys can be compared using memcmp (Julien Rouhaud) - Fix behavior with parallel queries (Julien Rouhaud based on a patch by Tomas Vondra) - Fix bug on TEXTCONST not being byval (Ronan Dunklau) - Fix 64bits counters on pass-by-ref float8 architectures (Julien Rouhaud) - Fix bug in pg_qualstats_names (Ronan Dunklau) - Fix bug in const position (Julien Rouhaud) - Fix pg_qualstats_pretty to use text instead of regoper, allowing usage of pg_upgrade when pg_qualstats is installed (Julien Rouhaud) - Fix segfault when interleaved executors cause bad sampling detection (Julien Rouhaud, reported by Andreas Seltenreich) Miscellaneous: - Add pg 10 compatibility (Julien Rouhaud) - Do not install docs anymore (Ronan Dunklau) - Add missing occurences/nbfiltered fields to pg_qualstats_pretty and pg_qualstats_all views (Julien Rouhaud) 1.0.2: Bugfix - Fix infinite loop for queries having a huge number of WHERE or JOIN clauses 1.0.1: Bugfix - Fix impossibility to install pg_qualstats if intarray extension is installed 1.0.0: Incompatibilites: - RenameGUC from sample_ratio to sample_rate Bugfix: - only get the exclusive lock on shared memory when needed - Fix bugs related to outer var resolution - Add missing function prototype Miscellaneous: - Add 9.6 compatibility - Code and comment cleanup Thanks to Thomas Vondra and Julien Rouhaud ! 0.0.9: - add sample_ratio hook - fix mistake while releasing 0.0.8 0.0.8: - add sample_ratio parameter 0.0.7: - fix counters for 32 bits builds - handle different collations for constants sampling - add a new "occurences" field, displaying the number of qual call - keep a unnormalized query string for each queryid - fix a bug with operator id retrieval - handles casts - add stats collection for nestedloops - handle FuncExpr and MinMaxExpr - improve performances for queries having multiple quals 0.0.6: - order quals and constants by their text positions. - fix bug with Index-Only Scans, which where not correctly supported - make pg_config configurable from the make invocation - ensure pg_qualstats is in shared_preload_libraries 0.0.5: - fix bug with = ANY(NULL) expressions 0.0.4: - add inline documentation in the sql script - fix a bug with 32bits builds (thanks to Alain Delorme for reporting it) pg_qualstats-1.0.9/CONTRIBUTORS.md000066400000000000000000000003121353412551700165110ustar00rootroot00000000000000 * Ronan Dunklau * Vik Fearing * Thomas Reiss * Julien Rouhaud * shribe * Tomas Vondra * Stéphane Tachoires * Pavel Trukhanov * Andreas Seltenreich * Nicolas Gollet * Gürkan Gür pg_qualstats-1.0.9/LICENSE000066400000000000000000000017461353412551700152530ustar00rootroot00000000000000Copyright (c) 2014-2017 Ronan Dunklau Copyright (c) 2018 The Powa-Team 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 COPYRIGHT HOLDER 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 COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE COPYRIGHT HOLDER 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 COPYRIGHT HOLDER HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pg_qualstats-1.0.9/META.json000066400000000000000000000011301353412551700156520ustar00rootroot00000000000000{ "name": "pg_qualstats", "abstract": "An extension collecting statistics about predicates", "version": "__VERSION__", "maintainer": "Ronan Dunklau ", "license": "postgresql", "release_status": "stable", "provides": { "pg_qualstats": { "abstract": "An extension collecting statistics about predicates", "file": "pg_qualstats.sql", "docfile": "doc/README.md", "version": "__VERSION__" } }, "meta-spec": { "version": "1.0.0", "url": "http://pgxn.org/meta/spec.txt" } } pg_qualstats-1.0.9/Makefile000066400000000000000000000015211353412551700156750ustar00rootroot00000000000000EXTENSION = pg_qualstats EXTVERSION = $(shell grep default_version $(EXTENSION).control | sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/") TESTS = $(wildcard test/sql/*.sql) REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) REGRESS_OPTS = --inputdir=test MODULES = $(patsubst %.c,%,$(wildcard *.c)) PG_CONFIG ?= pg_config all: release-zip: all git archive --format zip --prefix=pg_qualstats-$(EXTVERSION)/ --output ./pg_qualstats-$(EXTVERSION).zip HEAD unzip ./pg_qualstats-$(EXTVERSION).zip rm ./pg_qualstats-$(EXTVERSION).zip sed -i -e "s/__VERSION__/$(EXTVERSION)/g" ./pg_qualstats-$(EXTVERSION)/META.json zip -r ./pg_qualstats-$(EXTVERSION).zip ./pg_qualstats-$(EXTVERSION)/ rm ./pg_qualstats-$(EXTVERSION) -rf DATA = $(wildcard *--*.sql) PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) pg_qualstats-1.0.9/README.md000077700000000000000000000000001353412551700175342doc/README.mdustar00rootroot00000000000000pg_qualstats-1.0.9/debian/000077500000000000000000000000001353412551700154605ustar00rootroot00000000000000pg_qualstats-1.0.9/debian/changelog000066400000000000000000000023671353412551700173420ustar00rootroot00000000000000pg-qualstats (1.0.9-1) experimental; urgency=medium * New upstream version. -- Julien Rouhaud Thu, 05 Sep 2019 08:26:19 +0200 pg-qualstats (1.0.8-1) experimental; urgency=medium * New upstream version compatible with PG12. -- Christoph Berg Fri, 31 May 2019 14:12:34 +0200 pg-qualstats (1.0.7-1) unstable; urgency=medium [ Christoph Berg ] * Fix watch file to ignore debian/* tags. [ Julien Rouhaud ] * New upstream version -- Julien Rouhaud Thu, 15 Nov 2018 21:31:52 +0000 pg-qualstats (1.0.6-1) unstable; urgency=medium * New upstream version * Fix "broken symlink: /usr/share/doc/postgresql-11-pg- qualstats/README.md -> doc/README.md". The README.md is a symlink to doc/README.md, so just install doc/README.md. Thanks to Andreas Beckmann for the report! (Closes: #911476) -- Julien Rouhaud Sun, 21 Oct 2018 21:00:57 +0000 pg-qualstats (1.0.5-2) unstable; urgency=medium * Team upload. * Upload for PostgreSQL 11. -- Christoph Berg Fri, 12 Oct 2018 13:05:30 +0200 pg-qualstats (1.0.5-1) unstable; urgency=low * Initial release. -- Julien Rouhaud Sun, 22 Jul 2018 23:38:27 +0100 pg_qualstats-1.0.9/debian/compat000066400000000000000000000000021353412551700166560ustar00rootroot000000000000009 pg_qualstats-1.0.9/debian/control000066400000000000000000000016141353412551700170650ustar00rootroot00000000000000Source: pg-qualstats Section: database Priority: optional Maintainer: Julien Rouhaud Standards-Version: 4.1.3 Build-Depends: debhelper (>=9~), postgresql-server-dev-all (>= 141~) Homepage: https://powa.readthedocs.io/ Vcs-Browser: https://github.com/powa-team/pg_qualstats Vcs-Git: https://github.com/powa-team/pg_qualstats.git Package: postgresql-11-pg-qualstats Architecture: any Depends: ${misc:Depends}, ${shlibs:Depends}, postgresql-11 Description: PostgreSQL extension to gather statistics about predicates. This extensions tracks WHERE clauses predicates and JOIN predicates. Statistics will report whether the predicate was evaluated as an index scan or not, how many time the expression appeared, how many times the operator was executed and how filtering the expression is. If pg_stat_statements is enabled, it can also track to which statements the predicate belongs. pg_qualstats-1.0.9/debian/control.in000066400000000000000000000016311353412551700174710ustar00rootroot00000000000000Source: pg-qualstats Section: database Priority: optional Maintainer: Julien Rouhaud Standards-Version: 4.1.3 Build-Depends: debhelper (>=9~), postgresql-server-dev-all (>= 141~) Homepage: https://powa.readthedocs.io/ Vcs-Browser: https://github.com/powa-team/pg_qualstats Vcs-Git: https://github.com/powa-team/pg_qualstats.git Package: postgresql-PGVERSION-pg-qualstats Architecture: any Depends: ${misc:Depends}, ${shlibs:Depends}, postgresql-PGVERSION Description: PostgreSQL extension to gather statistics about predicates. This extensions tracks WHERE clauses predicates and JOIN predicates. Statistics will report whether the predicate was evaluated as an index scan or not, how many time the expression appeared, how many times the operator was executed and how filtering the expression is. If pg_stat_statements is enabled, it can also track to which statements the predicate belongs. pg_qualstats-1.0.9/debian/copyright000066400000000000000000000023211353412551700174110ustar00rootroot00000000000000Copyright (c) 2014-2017 Ronan Dunklau Copyright (c) 2018 The Powa-Team 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 COPYRIGHT HOLDER 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 COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE COPYRIGHT HOLDER 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 COPYRIGHT HOLDER HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. Contributors to pg_qualstats: * Ronan Dunklau * Julien ROuhaud * Tomas Tondra * Adrien Nayrat * Andreas Seltenreich * Stéphane Tachoires * shribe * Pavel Trukhanov * Nicolas Gollet * Thomas Reiss * Vik Fearing pg_qualstats-1.0.9/debian/pgversions000066400000000000000000000000051353412551700175750ustar00rootroot000000000000009.4+ pg_qualstats-1.0.9/debian/rules000077500000000000000000000014031353412551700165360ustar00rootroot00000000000000#!/usr/bin/make -f PKGVER = $(shell dpkg-parsechangelog | awk -F '[:-]' '/^Version:/ { print substr($$2, 2) }') EXCLUDE = --exclude-vcs --exclude=debian include /usr/share/postgresql-common/pgxs_debian_control.mk override_dh_auto_build: # do nothing override_dh_auto_test: # nothing to do here, upstream tests used, see debian/tests/* override_dh_auto_install: # build all supported versions +pg_buildext loop postgresql-%v-pg-qualstats override_dh_installdocs: dh_installdocs --all CONTRIBUTORS.md doc/README.md rm -rvf debian/*/usr/share/doc/postgresql-doc-* override_dh_installchangelogs: dh_installchangelogs CHANGELOG upstream orig: debian/control clean cd .. && tar czf pg-qualstats_$(PKGVER).orig.tar.gz $(EXCLUDE) pg-qualstats-$(PKGVER) %: dh $@ pg_qualstats-1.0.9/debian/source/000077500000000000000000000000001353412551700167605ustar00rootroot00000000000000pg_qualstats-1.0.9/debian/source/format000066400000000000000000000000041353412551700201650ustar00rootroot000000000000001.0 pg_qualstats-1.0.9/debian/tests/000077500000000000000000000000001353412551700166225ustar00rootroot00000000000000pg_qualstats-1.0.9/debian/tests/control000066400000000000000000000001251353412551700202230ustar00rootroot00000000000000Depends: @, postgresql-server-dev-all Tests: installcheck Restrictions: allow-stderr pg_qualstats-1.0.9/debian/tests/installcheck000077500000000000000000000001301353412551700212060ustar00rootroot00000000000000#!/bin/sh set -eu pg_buildext -o "shared_preload_libraries=pg_qualstats" installcheck pg_qualstats-1.0.9/debian/watch000066400000000000000000000001311353412551700165040ustar00rootroot00000000000000version=3 https://github.com/powa-team/pg_qualstats/releases .*/archive/([0-9].*).tar.gz pg_qualstats-1.0.9/doc/000077500000000000000000000000001353412551700150035ustar00rootroot00000000000000pg_qualstats-1.0.9/doc/README.md000066400000000000000000000226141353412551700162670ustar00rootroot00000000000000pg_qualstats ============ pg_qualstats is a PostgreSQL extension keeping statistics on predicates found in ```WHERE``` statements and ```JOIN``` clauses. This is useful if you want to be able to analyze what are the most-often executed quals (predicates) on your database. The [powa](http://powa.readthedocs.io/) project makes use of this to provide advances index suggestions. It also allows you to identify correlated columns, by identifying which columns are most frequently queried together. The extension works by looking for known patterns in queries. Currently, this includes: - Binary OpExpr where at least one side is a column from a table. Whenever possible, the predicate will be swaped so that CONST OP VAR expressions are turned into VAR COMMUTED_OP CONST. AND and OR expression members are counted as separate entries. Ex: WHERE column1 = 2, WHERE column1 = column2, WHERE 3 = column3 - ScalarArrayOpExpr where the left side is a VAR, and the right side is an array constant. Those will be counted one time per element in the array. Ex: WHERE column1 IN (2, 3) will be counted as 2 occurences for the (column1, '=') operator pair - BooleanTest where the expression is a simple boolean column reference Ex: WHERE column1 IS TRUE Please not that clauses like WHERE columns1, WHERE NOT column1 won't be processed by pg_qualstats (yet) This extension also saves the first query text, as-is, for each distinct queryid executed, with a limit of **pg_qualstats.max** entries. Please not that the gathered data are not saved when the PostgreSQL server is restarted. Installation ------------ - Compatible with PostgreSQL 9.4 or later - Needs postgresql header files - sudo make install - Add pg_qualstats to the shared preload libraries: ``` shared_preload_libraries = 'pg_qualstats' ``` Configuration ------------- The following GUCs can be configured, in postgresql.conf: - *pg_qualstats.enabled* (boolean, default true): whether or not pg_qualstats should be enabled - *pg_qualstats.track_constants* (bolean, default true): whether or not pg_qualstats should keep track of each constant value individually. Disabling this GUC will considerably reduce the number of entries necessary to keep track of predicates. - *pg_qualstats.max*: the maximum number of predicated and query text tracked (defaults to 1000) - *pg_qualstats.resolve_oids* (boolean, default false): whether or not pg_qualstats should resolve oids at query time, or juste store the oids. Enabling this parameter makes the data analysis much more easy, since a connection to the database where the query was executed won't be necessary, but it will eat much more space (624 bytes per entry instead of 176). Additionnaly, this will require some catalog lookups, which aren't free. - *pg_qualstats.track_pg_catalog* (boolean, default false): whether or not pg_qualstats should compute predicates on object in pg_catalog schema. - *pg_qualstats.sample_rate* (double, default -1): the fraction of queries that should be sampled. For example, 0.1 means that only one out of ten queries will be sampled. The default (-1) means automatic, and results in a value of 1 / max_connections, so that statiscally, concurrency issues will be rare. Usage ----- - Create the extension in any database: ``` CREATE EXTENSION pg_qualstats; ``` ### Functions The extension defines the following functions: - **pg_qualstats**: returns the counts for every qualifier, identified by the expression hash. This hash identifies each expression. - *userid*: oid of the user who executed the query. - *dbid*: oid of the database in which the query has been executed. - *lrelid*, *lattnum*: oid of the relation and attribute number of the VAR on the left hand side, if any. - *opno*: oid of the operator used in the expression - *rrelid*, *rattnum*: oid of the relation and attribute number of the VAR on the right hand side, if any. - *qualid*: normalized identifier of the parent "AND" expression, if any. This identifier is computed excluding the constants. This is useful for identifying predicates which are used together. - *uniquequalid*: unique identifier of the parent "AND" expression, if any. This identifier is computed including the constants. - *qualnodeid*: normalized identifier of this simple predicate. This identifier is computed excluding the constants. - *uniquequalnodeid*: unique identifier of this simple predicate. This identifier is computed including the constats. - *occurences*: number of time this predicate has been invoked, ie. number of related query execution. - *execution_count*: number of time this predicate has been executed, ie. number of rows it processed. - *nbfiltered*: number of tuples this predicate discarded. - *constant_position*: location of the constant in the original query string, as reported by the parser. - *queryid*: if pg_stats_statements is installed, the queryid identifying this query, otherwise NULL. - *constvalue*: a string representation of the right-hand side constant, if any, truncated to 80 bytes. - *eval_type*: evaluation type. 'f' for a predicate evaluated after a scan or 'i' for an index predicate. Example: ``` ro=# select * from pg_qualstats; userid │ dbid │ lrelid │ lattnum │ opno │ rrelid │ rattnum │ qualid │ uniquequalid │ qualnodeid │ uniquequalnodeid │ occurences │ execution_count │ nbfiltered │ constant_position │ queryid │ constvalue │ eval_type --------+-------+--------+---------+------+--------+---------+--------+--------------+------------+------------------+------------+-----------------+------------+-------------------+---------+----------------+----------- 10 │ 16384 │ 16385 │ 2 │ 98 │ │ 115075651 │ 1858640877 │ 1 │ 100000 │ 99999 │ 29 │ │ 'line 1'::text │ f 10 │ 16384 │ 16391 │ 2 │ 98 │ 16385 │ 2 │ │ 497379130 │ 497379130 │ 1 │ 0 │ 0 │ │ │ f ``` - **pg_qualstats_exemple_queries**: return all the stored query texts. - **pg_qualstats_exemple_query**: return the stored query text for the given queryid if any, otherwise NULL. - **pg_qualstats_names**: return all the stored query texts. - **pg_qualstats_reset**: reset the internal counters and forget about every encountered qual. ### Views In addition to that, the extension defines some views on top of the pg_qualstats function: - **pg_qualstats**: filters calls to pg_qualstats() by the current database. - **pg_qualstats_pretty**: performs the appropriate joins to display a readable aggregated form for every attribute from the pg_qualstats view Example: ``` ro=# select * from pg_qualstats_pretty; left_schema | left_table | left_column | operator | right_schema | right_table | right_column | occurences | execution_count | nbfiltered -------------+------------------+-------------+--------------+--------------+-------------+--------------+------------+-----------------+------------ public | pgbench_accounts | aid | pg_catalog.= | | | | 5 | 5000000 | 4999995 public | pgbench_tellers | tid | pg_catalog.= | | | | 10 | 10000000 | 9999990 public | pgbench_branches | bid | pg_catalog.= | | | | 10 | 2000000 | 1999990 public | t1 | id | pg_catalog.= | public | t2 | id_t1 | 1 | 10000 | 9999 ``` - **pg_qualstats_all**: sums the counts for each attribute / operator pair, regardless of its position as an operand (LEFT or RIGHT), grouping together attributes used in AND clauses. Example: ``` ro=# select * from pg_qualstats_all; dbid | relid | userid | queryid | attnums | opno | qualid | occurences | execution_count | nbfiltered | qualnodeid -------+-------+--------+---------+---------+------+--------+------------+-----------------+------------+------------ 16384 | 16385 | 10 | | {2} | 98 | | 1 | 100000 | 99999 | 115075651 16384 | 16391 | 10 | | {2} | 98 | | 2 | 0 | 0 | 497379130 ``` - **pg_qualstats_indexes**: looks up those attributes for which an index doesn't exist with the attribute in first position. It's a very simple and naive search, if you're interested in more advanced missing index detection, look at the [powa](http://powa.readthedocs.io/) project. Example: ``` ro=# select * from pg_qualstats_indexes; relid | attnames | possible_types | execution_count ------------------+-----------------------------+----------------+----------------- pgbench_accounts | {filler} | {btree,hash} | 5 pgbench_accounts | {bid} | {btree,hash} | 2 pgbench_accounts | {bid,filler} | {btree,hash} | 8 (9 rows) ``` - **pg_qualstats_by_query**: returns only predicates of the form VAR OPERATOR CONSTANT, aggregated by queryid. pg_qualstats-1.0.9/expected/000077500000000000000000000000001353412551700160375ustar00rootroot00000000000000pg_qualstats-1.0.9/expected/pg_qualstats.out000066400000000000000000000020031353412551700212720ustar00rootroot00000000000000CREATE EXTENSION pg_qualstats; -- Make sure sure we'll see at least one qual SET pg_qualstats.sample_rate = 1; CREATE TABLE pgqs AS SELECT id FROM generate_series(1, 100) id; SELECT COUNT(*) FROM pgqs WHERE id = 1; count ------- 1 (1 row) SELECT lrelid::regclass::text, lattnum, occurences, execution_count, nbfiltered, constvalue, eval_type FROM pg_qualstats; lrelid | lattnum | occurences | execution_count | nbfiltered | constvalue | eval_type --------+---------+------------+-----------------+------------+------------+----------- pgqs | 1 | 1 | 100 | 99 | 1::integer | f (1 row) SELECT COUNT(*) > 0 FROM pg_qualstats; ?column? ---------- t (1 row) SELECT COUNT(*) > 0 FROM pg_qualstats(); ?column? ---------- t (1 row) SELECT COUNT(*) > 0 FROM pg_qualstats_example_queries(); ?column? ---------- t (1 row) SELECT pg_qualstats_reset(); pg_qualstats_reset -------------------- (1 row) SELECT COUNT(*) FROM pg_qualstats(); count ------- 0 (1 row) pg_qualstats-1.0.9/pg_qualstats--1.0.9.sql000066400000000000000000000362201353412551700202110ustar00rootroot00000000000000/*""" .. function:: pg_qualstats_reset() Resets statistics gathered by pg_qualstats. */ CREATE FUNCTION pg_qualstats_reset() RETURNS void AS 'MODULE_PATHNAME' LANGUAGE C; /*""" .. function pg_qualstats_example_query(bigint) Returns an example for a normalized query, given its queryid */ CREATE FUNCTION pg_qualstats_example_query(bigint) RETURNS text AS 'MODULE_PATHNAME' LANGUAGE C; /*""" .. function pg_qualstats_example_queries() Returns all the example queries with their associated queryid */ CREATE FUNCTION pg_qualstats_example_queries(OUT queryid bigint, OUT query text) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C; /*""" .. function:: pg_qualstats() Returns: A SETOF record containing the data gathered by pg_qualstats Attributes: userid (oid): the user who executed the query dbid (oid): the database on which the query was executed lrelid (oid): oid of the relation on the left hand side lattnum (attnum): attribute number of the column on the left hand side opno (oid): oid of the operator used in the expression rrelid (oid): oid of the relation on the right hand side rattnum (attnum): attribute number of the column on the right hand side uniquequalnodeid(bigint): hash of the parent ``AND`` expression, if any. This is useful for identifying predicates which are used together. qualnodeid(bigint): the predicate hash. Everything (down to constants) is used to compute this hash occurences (bigint): the number of times this predicate has been seen execution_count (bigint): the total number of execution of this predicate. nbfiltered (bigint): the number of lines filtered by this predicate constant_position (int): the position of the constant in the original query, as filled by the lexer. queryid (bigint): the queryid identifying this query, as generated by pg_stat_statements constvalue (varchar): a string representation of the right-hand side constant, if any, truncated to 80 bytes. eval_type (char): the evaluation type. Possible values are ``f`` for execution as a filter (ie, after a Scan) or ``i`` if it was evaluated as an index predicate. If the qual is evaluated as an index predicate, then the nbfiltered value will most likely be 0, except if there was any rechecked conditions. Example: .. code-block:: sql powa=# select * from powa_statements where queryid != 2; powa=# select * from pg_qualstats(); -[ RECORD 1 ]-----+----------- userid | 16384 dbid | 850774 lrelid | 851367 lattnum | 1 opno | 417 rrelid | rattnum | qualid | uniquequalid | qualnodeid | 1711571257 uniquequalnodeid | 466568149 occurences | 1 execution_count | 1206 nbfiltered | 0 constant_position | 47 queryid | 3644521490 constvalue | 2::integer eval_type | f */ CREATE FUNCTION pg_qualstats( OUT userid oid, OUT dbid oid, OUT lrelid oid, OUT lattnum smallint, OUT opno oid, OUT rrelid oid, OUT rattnum smallint, OUT qualid bigint, OUT uniquequalid bigint, OUT qualnodeid bigint, OUT uniquequalnodeid bigint, OUT occurences bigint, OUT execution_count bigint, OUT nbfiltered bigint, OUT constant_position int, OUT queryid bigint, OUT constvalue varchar, OUT eval_type "char" ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; /*""" .. function:: pg_qualstats_names() This function is the same as pg_qualstats, but with additional columns corresponding to the resolved names, if ``pg_qualstats.resolve_oids`` is set to ``true``. Returns: The same set of columns than :func:`pg_qualstats()`, plus the following ones: rolname (text): the name of the role executing the query. Corresponds to userid. dbname (text): the name of the database on which the query was executed. Corresponds to dbid. lrelname (text): the name of the relation on the left-hand side of the qual. Corresponds to lrelid. lattname (text): the name of the attribute (column) on the left-hand side of the qual. Corresponds to rrelid. opname (text): the name of the operator. Corresponds to opno. */ CREATE FUNCTION pg_qualstats_names( OUT userid oid, OUT dbid oid, OUT lrelid oid, OUT lattnum smallint, OUT opno oid, OUT rrelid oid, OUT rattnum smallint, OUT qualid bigint, OUT uniquequalid bigint, OUT qualnodeid bigint, OUT uniquequalnodeid bigint, OUT occurences bigint, OUT execution_count bigint, OUT nbfiltered bigint, OUT constant_position int, OUT queryid bigint, OUT constvalue varchar, OUT eval_type "char", OUT rolname text, OUT dbname text, OUT lrelname text, OUT lattname text, OUT opname text, OUT rrelname text, OUT rattname text ) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C STRICT VOLATILE; -- Register a view on the function for ease of use. /*""" .. view:: pg_qualstats This view is just a simple wrapper on the :func:`pg_qualstats()` function, filtering on the current database for convenience. */ CREATE VIEW pg_qualstats AS SELECT qs.* FROM pg_qualstats() qs INNER JOIN pg_database on qs.dbid = pg_database.oid WHERE pg_database.datname = current_database(); GRANT SELECT ON pg_qualstats TO PUBLIC; -- Don't want this to be available to non-superusers. REVOKE ALL ON FUNCTION pg_qualstats_reset() FROM PUBLIC; /*""" .. view:: pg_qualstats_pretty This view resolves oid "on the fly", for the current database. Returns: left_schema (name): the name of the left-hand side relation's schema. left_table (name): the name of the left-hand side relation. left_column (name): the name of the left-hand side attribute. operator (name): the name of the operator. right_schema (name): the name of the right-hand side relation's schema. right_table (name): the name of the right-hand side relation. right_column (name): the name of the operator. execution_count (bigint): the total number of time this qual was executed. nbfiltered (bigint): the total number of tuples filtered by this qual. */ CREATE VIEW pg_qualstats_pretty AS select nl.nspname as left_schema, al.attrelid::regclass as left_table, al.attname as left_column, opno::regoper::text as operator, nr.nspname as right_schema, ar.attrelid::regclass as right_table, ar.attname as right_column, sum(occurences) as occurences, sum(execution_count) as execution_count, sum(nbfiltered) as nbfiltered from pg_qualstats qs left join (pg_class cl inner join pg_namespace nl on nl.oid = cl.relnamespace) on cl.oid = qs.lrelid left join (pg_class cr inner join pg_namespace nr on nr.oid = cr.relnamespace) on cr.oid = qs.rrelid left join pg_attribute al on al.attrelid = qs.lrelid and al.attnum = qs.lattnum left join pg_attribute ar on ar.attrelid = qs.rrelid and ar.attnum = qs.rattnum group by al.attrelid, al.attname, ar.attrelid, ar.attname, opno, nl.nspname, nr.nspname ; CREATE OR REPLACE VIEW pg_qualstats_all AS SELECT dbid, relid, userid, queryid, array_agg(distinct attnum) as attnums, opno, max(qualid) as qualid, sum(occurences) as occurences, sum(execution_count) as execution_count, sum(nbfiltered) as nbfiltered, coalesce(qualid, qualnodeid) as qualnodeid FROM ( SELECT qs.dbid, CASE WHEN lrelid IS NOT NULL THEN lrelid WHEN rrelid IS NOT NULL THEN rrelid END as relid, qs.userid as userid, CASE WHEN lrelid IS NOT NULL THEN lattnum WHEN rrelid IS NOT NULL THEN rattnum END as attnum, qs.opno as opno, qs.qualid as qualid, qs.qualnodeid as qualnodeid, qs.occurences as occurences, qs.execution_count as execution_count, qs.nbfiltered as nbfiltered, qs.queryid FROM pg_qualstats() qs WHERE lrelid IS NOT NULL or rrelid IS NOT NULL ) t GROUP BY dbid, relid, userid, queryid, opno, coalesce(qualid, qualnodeid) ; /*""" .. type:: qual Attributes: relid (oid): the relation oid attnum (integer): the attribute number opno (oid): the operator oid eval_type (char): the evaluation type. See :func:`pg_qualstats()` for an explanation of the eval_type. */ CREATE TYPE qual AS ( relid oid, attnum integer, opno oid, eval_type "char" ); /*""" .. type:: qualname Pendant of :type:`qual`, but with names instead of oids Attributes: relname (text): the relation oid attname (text): the attribute number opname (text): the operator name eval_type (char): the evaluation type. See :func:`pg_qualstats()` for an explanation of the eval_type. */ CREATE TYPE qualname AS ( relname text, attnname text, opname text, eval_type "char" ); CREATE OR REPLACE VIEW pg_qualstats_by_query AS SELECT coalesce(uniquequalid, uniquequalnodeid) as uniquequalnodeid, dbid, userid, coalesce(qualid, qualnodeid) as qualnodeid, occurences, execution_count, nbfiltered, queryid, array_agg(constvalue order by constant_position) as constvalues, array_agg(ROW(relid, attnum, opno, eval_type)::qual) as quals FROM ( SELECT qs.dbid, CASE WHEN lrelid IS NOT NULL THEN lrelid WHEN rrelid IS NOT NULL THEN rrelid END as relid, qs.userid as userid, CASE WHEN lrelid IS NOT NULL THEN lattnum WHEN rrelid IS NOT NULL THEN rattnum END as attnum, qs.opno as opno, qs.qualid as qualid, qs.uniquequalid as uniquequalid, qs.qualnodeid as qualnodeid, qs.uniquequalnodeid as uniquequalnodeid, qs.occurences as occurences, qs.execution_count as execution_count, qs.queryid as queryid, qs.constvalue as constvalue, qs.nbfiltered as nbfiltered, qs.eval_type, qs.constant_position FROM pg_qualstats() qs WHERE (qs.lrelid IS NULL) != (qs.rrelid IS NULL) ) i GROUP BY coalesce(uniquequalid, uniquequalnodeid), coalesce(qualid, qualnodeid), dbid, userid, occurences, execution_count, nbfiltered, queryid ; CREATE VIEW pg_qualstats_indexes AS SELECT relid::regclass, attnames, possible_types, sum(execution_count) as execution_count FROM ( SELECT qs.relid::regclass, array_agg(distinct attnames) as attnames, array_agg(distinct amname) as possible_types, max(execution_count) as execution_count, array_agg(distinct attnum) as attnums FROM pg_qualstats_all as qs INNER JOIN pg_amop amop ON amop.amopopr = opno INNER JOIN pg_am on amop.amopmethod = pg_am.oid, LATERAL (SELECT attname as attnames from pg_attribute inner join unnest(attnums) a on a = attnum and attrelid = qs.relid order by attnum) as attnames, LATERAL unnest(attnums) as attnum WHERE NOT EXISTS ( SELECT 1 from pg_index i WHERE indrelid = relid AND ( arraycontains((i.indkey::int[])[0:array_length(attnums, 1) - 1], (attnums::int[])) OR (arraycontains((attnums::int[]),(i.indkey::int[])[0:array_length(indkey, 1) + 1]) AND i.indisunique)) ) GROUP BY qs.relid, qualnodeid ) t GROUP BY relid, attnames, possible_types; CREATE OR REPLACE FUNCTION pg_qualstats_suggest_indexes(relid oid, attnums integer[], opno oid) RETURNS TABLE(index_ddl text) AS $$ BEGIN RETURN QUERY SELECT 'CREATE INDEX idx_' || q.relid || '_' || array_to_string(attnames, '_') || ' ON ' || nspname || '.' || q.relid || ' USING ' || idxtype || ' (' || array_to_string(attnames, ', ') || ')' AS index_ddl FROM (SELECT t.nspname, t.relid, t.attnames, unnest(t.possible_types) AS idxtype FROM ( SELECT nl.nspname AS nspname, qs.relid::regclass AS relid, array_agg(DISTINCT attnames.attnames) AS attnames, array_agg(DISTINCT pg_am.amname) AS possible_types, array_agg(DISTINCT attnum.attnum) AS attnums FROM (VALUES (relid, attnums::int[], opno)) as qs(relid, attnums, opno) LEFT JOIN (pg_class cl JOIN pg_namespace nl ON nl.oid = cl.relnamespace) ON cl.oid = qs.relid JOIN pg_am amop ON amop.amopopr = qs.opno JOIN pg_am ON amop.amopmethod = pg_am.oid AND pg_am.amname <> 'hash', LATERAL ( SELECT pg_attribute.attname AS attnames FROM pg_attribute JOIN unnest(qs.attnums) a(a) ON a.a = pg_attribute.attnum AND pg_attribute.attrelid = qs.relid ORDER BY pg_attribute.attnum) attnames, LATERAL unnest(qs.attnums) attnum(attnum) WHERE NOT (EXISTS ( SELECT 1 FROM pg_index i WHERE i.indrelid = qs.relid AND (arraycontains((i.indkey::int[])[0:array_length(qs.attnums, 1) - 1], qs.attnums::int[]) OR arraycontains(qs.attnums::int[], (i.indkey::int[])[0:array_length(i.indkey, 1) + 1]) AND i.indisunique))) GROUP BY nl.nspname, qs.relid) t GROUP BY t.nspname, t.relid, t.attnames, t.possible_types) q; END; $$ language plpgsql; CREATE OR REPLACE VIEW pg_qualstats_indexes_ddl AS SELECT q.nspname, q.relid, q.attnames, q.idxtype, q.execution_count, 'CREATE INDEX idx_' || relid || '_' || array_to_string(attnames, '_') || ' ON ' || nspname || '.' || relid || ' USING ' || idxtype || ' (' || array_to_string(attnames, ', ') || ')' AS ddl FROM (SELECT t.nspname, t.relid, t.attnames, unnest(t.possible_types) AS idxtype, sum(t.execution_count) AS execution_count FROM ( SELECT nl.nspname AS nspname, qs.relid::regclass AS relid, array_agg(DISTINCT attnames.attnames) AS attnames, array_agg(DISTINCT pg_am.amname) AS possible_types, max(qs.execution_count) AS execution_count, array_agg(DISTINCT attnum.attnum) AS attnums FROM pg_qualstats_all qs LEFT JOIN (pg_class cl JOIN pg_namespace nl ON nl.oid = cl.relnamespace) ON cl.oid = qs.relid JOIN pg_amop amop ON amop.amopopr = qs.opno JOIN pg_am ON amop.amopmethod = pg_am.oid, LATERAL ( SELECT pg_attribute.attname AS attnames FROM pg_attribute JOIN unnest(qs.attnums) a(a) ON a.a = pg_attribute.attnum AND pg_attribute.attrelid = qs.relid ORDER BY pg_attribute.attnum) attnames, LATERAL unnest(qs.attnums) attnum(attnum) WHERE NOT (EXISTS ( SELECT 1 FROM pg_index i WHERE i.indrelid = qs.relid AND (arraycontains((i.indkey::int[])[0:array_length(qs.attnums, 1) - 1], qs.attnums::int[]) OR arraycontains(qs.attnums::int[], (i.indkey::int[])[0:array_length(i.indkey, 1) + 1]) AND i.indisunique))) GROUP BY nl.nspname, qs.relid, qs.qualnodeid) t GROUP BY t.nspname, t.relid, t.attnames, t.possible_types) q; pg_qualstats-1.0.9/pg_qualstats.c000066400000000000000000001615231353412551700171210ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pg_qualstats.c * Track frequently used quals. * * This extension works by installing a hooks on executor. * The ExecutorStart hook will enable some instrumentation for the * queries (INSTRUMENT_ROWS and INSTRUMENT_BUFFERS). * * The ExecutorEnd hook will look for every qual in the query, and * stores the quals of the form: * - EXPR OPERATOR CONSTANT * - EXPR OPERATOR EXPR * * If pg_stat_statements is available, the statistics will be * aggregated by queryid, and a not-normalized statement will be * stored for each different queryid. This can allow third part tools * to do some work on a real query easily. * * The implementation is heavily inspired by pg_stat_statements * * Copyright (c) 2014,2017 Ronan Dunklau * Copyright (c) 2018, The Powa-Team *------------------------------------------------------------------------- */ #include #include "postgres.h" #include "access/hash.h" #include "access/htup_details.h" #if PG_VERSION_NUM >= 90600 #include "access/parallel.h" #endif #include "catalog/pg_class.h" #include "catalog/pg_namespace.h" #include "catalog/pg_operator.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" #include "fmgr.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/execnodes.h" #include "nodes/nodeFuncs.h" #include "nodes/makefuncs.h" #include "optimizer/clauses.h" #include "optimizer/planner.h" #include "parser/analyze.h" #include "parser/parse_node.h" #include "parser/parsetree.h" #include "postmaster/autovacuum.h" #include "storage/ipc.h" #include "storage/lwlock.h" #if PG_VERSION_NUM >= 100000 #include "storage/shmem.h" #endif #include "utils/array.h" #include "utils/builtins.h" #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" #include "utils/tuplestore.h" PG_MODULE_MAGIC; #define PGQS_COLUMNS 18 /* number of columns in pg_qualstats SRF */ #define PGQS_NAME_COLUMNS 7 /* number of column added when using * pg_qualstats_column SRF */ #define PGQS_USAGE_DEALLOC_PERCENT 5 /* free this % of entries at once */ #define PGQS_MAX_LOCAL_ENTRIES (pgqs_max * 0.2) /* do not track more of * 20% of possible entries * in shared mem */ #define PGQS_CONSTANT_SIZE 80 /* Truncate constant representation at 80 */ #define PGQS_FLAGS (INSTRUMENT_ROWS|INSTRUMENT_BUFFERS) /*---- Function declarations ----*/ void _PG_init(void); void _PG_fini(void); extern Datum pg_qualstats_reset(PG_FUNCTION_ARGS); extern Datum pg_qualstats(PG_FUNCTION_ARGS); extern Datum pg_qualstats_names(PG_FUNCTION_ARGS); static Datum pg_qualstats_common(PG_FUNCTION_ARGS, bool include_names); extern Datum pg_qualstats_example_query(PG_FUNCTION_ARGS); extern Datum pg_qualstats_example_queries(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(pg_qualstats_reset); PG_FUNCTION_INFO_V1(pg_qualstats); PG_FUNCTION_INFO_V1(pg_qualstats_names); PG_FUNCTION_INFO_V1(pg_qualstats_example_query); PG_FUNCTION_INFO_V1(pg_qualstats_example_queries); static void pgqs_shmem_startup(void); static void pgqs_ExecutorStart(QueryDesc *queryDesc, int eflags); static void pgqs_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, #if PG_VERSION_NUM >= 90600 uint64 count #else long count #endif #if PG_VERSION_NUM >= 100000 ,bool execute_once #endif ); static void pgqs_ExecutorFinish(QueryDesc *queryDesc); static void pgqs_ExecutorEnd(QueryDesc *queryDesc); static ExecutorStart_hook_type prev_ExecutorStart = NULL; static ExecutorRun_hook_type prev_ExecutorRun = NULL; static ExecutorFinish_hook_type prev_ExecutorFinish = NULL; static ExecutorEnd_hook_type prev_ExecutorEnd = NULL; static shmem_startup_hook_type prev_shmem_startup_hook = NULL; static uint32 pgqs_hash_fn(const void *key, Size keysize); #if PG_VERSION_NUM < 90500 static uint32 pgqs_uint32_hashfn(const void *key, Size keysize); #endif static int pgqs_query_size; static int pgqs_max; /* max # statements to track */ static bool pgqs_track_pgcatalog; /* track queries on pg_catalog */ static bool pgqs_resolve_oids; /* resolve oids */ static bool pgqs_enabled; static bool pgqs_track_constants; static double pgqs_sample_rate; static int query_is_sampled; /* Is the current query sampled, per backend */ static int nesting_level = 0; /* Current nesting depth of ExecutorRun calls */ static bool pgqs_assign_sample_rate_check_hook(double *newval, void **extra, GucSource source); #if PG_VERSION_NUM > 90600 static void pgqs_set_query_sampled(bool sample); #endif static bool pgqs_is_query_sampled(void); /*---- Data structures declarations ----*/ typedef struct pgqsSharedState { #if PG_VERSION_NUM >= 90400 LWLock *lock; /* protects counters hashtable * search/modification */ LWLock *querylock; /* protects query hashtable * search/modification */ #else LWLockId lock; /* protects counters hashtable * search/modification */ LWLockId querylock; /* protects query hashtable * search/modification */ #endif #if PG_VERSION_NUM >= 90600 LWLock *sampledlock; /* protects sampled array search/modification */ bool sampled[FLEXIBLE_ARRAY_MEMBER]; /* should we sample this * query? */ #endif } pgqsSharedState; /* Since cff440d368, queryid becomes a uint64 internally. */ #if PG_VERSION_NUM >= 110000 typedef uint64 pgqs_queryid; #else typedef uint32 pgqs_queryid; #endif typedef struct pgqsHashKey { Oid userid; /* user OID */ Oid dbid; /* database OID */ pgqs_queryid queryid; /* query identifier (if set by another plugin */ uint32 uniquequalnodeid; /* Hash of the const */ uint32 uniquequalid; /* Hash of the parent, including the consts */ char evaltype; /* Evaluation type. Can be 'f' to mean a qual * executed after a scan, or 'i' for an * indexqual */ } pgqsHashKey; typedef struct pgqsNames { NameData rolname; NameData datname; NameData lrelname; NameData lattname; NameData opname; NameData rrelname; NameData rattname; } pgqsNames; typedef struct pgqsEntry { pgqsHashKey key; Oid lrelid; /* relation OID or NULL if not var */ AttrNumber lattnum; /* Attribute Number or NULL if not var */ Oid opoid; /* Operator OID */ Oid rrelid; /* relation OID or NULL if not var */ AttrNumber rattnum; /* Attribute Number or NULL if not var */ char constvalue[PGQS_CONSTANT_SIZE]; /* Textual representation of * the right hand constant, if * any */ uint32 qualid; /* Hash of the parent AND expression if any, 0 * otherwise. */ uint32 qualnodeid; /* Hash of the node itself */ int64 count; /* # of operator execution */ int64 nbfiltered; /* # of lines discarded by the operator */ int position; /* content position in query text */ double usage; /* # of qual execution, used for deallocation */ int64 occurences; } pgqsEntry; typedef struct pgqsEntryWithNames { pgqsEntry entry; pgqsNames names; } pgqsEntryWithNames; typedef struct pgqsQueryStringHashKey { pgqs_queryid queryid; } pgqsQueryStringHashKey; typedef struct pgqsQueryStringEntry { pgqsQueryStringHashKey key; /* * Imperatively at the end of the struct This is actually of length * query_size, which is track_activity_query_size */ char querytext[1]; } pgqsQueryStringEntry; /* * Transient state of the query tree walker - for the meaning of the counters, * see pgqsEntry comments. */ typedef struct pgqsWalkerContext { pgqs_queryid queryId; List *rtable; PlanState *planstate; PlanState *inner_planstate; PlanState *outer_planstate; List *outer_tlist; List *inner_tlist; List *index_tlist; uint32 qualid; uint32 uniquequalid; /* Hash of the parent, including the consts */ int64 count; int64 nbfiltered; int nentries; /* number of entries found so far */ char evaltype; const char *querytext; } pgqsWalkerContext; static bool pgqs_whereclause_tree_walker(Node *node, pgqsWalkerContext *query); static pgqsEntry *pgqs_process_opexpr(OpExpr *expr, pgqsWalkerContext *context); static pgqsEntry *pgqs_process_scalararrayopexpr(ScalarArrayOpExpr *expr, pgqsWalkerContext *context); static pgqsEntry *pgqs_process_booltest(BooleanTest *expr, pgqsWalkerContext *context); static void pgqs_collectNodeStats(PlanState *planstate, List *ancestors, pgqsWalkerContext *context); static void pgqs_collectMemberNodeStats(int nplans, PlanState **planstates, List *ancestors, pgqsWalkerContext *context); static void pgqs_collectSubPlanStats(List *plans, List *ancestors, pgqsWalkerContext *context); static uint32 hashExpr(Expr *expr, pgqsWalkerContext *context, bool include_const); static void exprRepr(Expr *expr, StringInfo buffer, pgqsWalkerContext *context, bool include_const); static void pgqs_set_planstates(PlanState *planstate, pgqsWalkerContext *context); static Expr *pgqs_resolve_var(Var *var, pgqsWalkerContext *context); static void pgqs_entry_dealloc(void); static void pgqs_queryentry_dealloc(void); static void pgqs_localentry_dealloc(int nvictims); static void pgqs_fillnames(pgqsEntryWithNames *entry); static Size pgqs_memsize(void); #if PG_VERSION_NUM >= 90600 static Size pgqs_sampled_array_size(void); #endif /* Global Hash */ static HTAB *pgqs_hash = NULL; static HTAB *pgqs_query_examples_hash = NULL; static pgqsSharedState *pgqs = NULL; /* Local Hash */ static HTAB *pgqs_localhash = NULL; void _PG_init(void) { if (!process_shared_preload_libraries_in_progress) { elog(ERROR, "This module can only be loaded via shared_preload_libraries"); return; } prev_ExecutorStart = ExecutorStart_hook; ExecutorStart_hook = pgqs_ExecutorStart; prev_ExecutorRun = ExecutorRun_hook; ExecutorRun_hook = pgqs_ExecutorRun; prev_ExecutorFinish = ExecutorFinish_hook; ExecutorFinish_hook = pgqs_ExecutorFinish; prev_ExecutorEnd = ExecutorEnd_hook; ExecutorEnd_hook = pgqs_ExecutorEnd; prev_shmem_startup_hook = shmem_startup_hook; shmem_startup_hook = pgqs_shmem_startup; DefineCustomBoolVariable("pg_qualstats.enabled", "Enable / Disable pg_qualstats", NULL, &pgqs_enabled, true, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomBoolVariable("pg_qualstats.track_constants", "Enable / Disable pg_qualstats constants tracking", NULL, &pgqs_track_constants, true, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("pg_qualstats.max", "Sets the maximum number of statements tracked by pg_qualstats.", NULL, &pgqs_max, 1000, 100, INT_MAX, PGC_POSTMASTER, 0, NULL, NULL, NULL); DefineCustomBoolVariable("pg_qualstats.resolve_oids", "Store names alongside the oid. Eats MUCH more space!", NULL, &pgqs_resolve_oids, false, PGC_POSTMASTER, 0, NULL, NULL, NULL); DefineCustomBoolVariable("pg_qualstats.track_pg_catalog", "Track quals on system catalogs too.", NULL, &pgqs_track_pgcatalog, false, PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomRealVariable("pg_qualstats.sample_rate", "Sampling rate. 1 means every query, 0.2 means 1 in five queries", NULL, &pgqs_sample_rate, -1, -1, 1, PGC_USERSET, 0, pgqs_assign_sample_rate_check_hook, NULL, NULL); EmitWarningsOnPlaceholders("pg_qualstats"); parse_int(GetConfigOption("track_activity_query_size", false, false), &pgqs_query_size, 0, NULL); RequestAddinShmemSpace(pgqs_memsize()); #if PG_VERSION_NUM >= 90600 RequestNamedLWLockTranche("pg_qualstats", 3); #else RequestAddinLWLocks(2); #endif } void _PG_fini(void) { /* Uninstall hooks. */ shmem_startup_hook = prev_shmem_startup_hook; ExecutorStart_hook = prev_ExecutorStart; ExecutorRun_hook = prev_ExecutorRun; ExecutorFinish_hook = prev_ExecutorFinish; ExecutorEnd_hook = prev_ExecutorEnd; } /* * Check that the sample ratio is in the correct interval */ static bool pgqs_assign_sample_rate_check_hook(double *newval, void **extra, GucSource source) { double val = *newval; if ((val < 0 && val != -1) || (val > 1)) return false; if (val == -1) *newval = 1. / MaxConnections; return true; } #if PG_VERSION_NUM >= 90600 static void pgqs_set_query_sampled(bool sample) { /* the decisions should only be made in leader */ Assert(!IsParallelWorker()); /* in worker processes we need to get the info from shared memory */ LWLockAcquire(pgqs->sampledlock, LW_EXCLUSIVE); pgqs->sampled[MyBackendId] = sample; LWLockRelease(pgqs->sampledlock); } #endif static bool pgqs_is_query_sampled(void) { #if PG_VERSION_NUM >= 90600 bool sampled; /* in leader we can just check the global variable */ if (!IsParallelWorker()) return query_is_sampled; /* in worker processes we need to get the info from shared memory */ LWLockAcquire(pgqs->sampledlock, LW_SHARED); sampled = pgqs->sampled[ParallelMasterBackendId]; LWLockRelease(pgqs->sampledlock); return sampled; #else return query_is_sampled; #endif } /* * Do catalog search to replace oids with corresponding objects name */ void pgqs_fillnames(pgqsEntryWithNames *entry) { HeapTuple tp; #if PG_VERSION_NUM >= 90500 namestrcpy(&(entry->names.rolname), GetUserNameFromId(entry->entry.key.userid, true)); #else namestrcpy(&(entry->names.rolname), GetUserNameFromId(entry->entry.key.userid)); #endif namestrcpy(&(entry->names.datname), get_database_name(entry->entry.key.dbid)); if (entry->entry.lrelid != InvalidOid) { tp = SearchSysCache1(RELOID, ObjectIdGetDatum(entry->entry.lrelid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "Invalid lreloid"); namecpy(&(entry->names.lrelname), &(((Form_pg_class) GETSTRUCT(tp))->relname)); ReleaseSysCache(tp); tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(entry->entry.lrelid), entry->entry.lattnum); if (!HeapTupleIsValid(tp)) elog(ERROR, "Invalid lattr"); namecpy(&(entry->names.lattname), &(((Form_pg_attribute) GETSTRUCT(tp))->attname)); ReleaseSysCache(tp); } if (entry->entry.opoid != InvalidOid) { tp = SearchSysCache1(OPEROID, ObjectIdGetDatum(entry->entry.opoid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "Invalid operator"); namecpy(&(entry->names.opname), &(((Form_pg_operator) GETSTRUCT(tp))->oprname)); ReleaseSysCache(tp); } if (entry->entry.rrelid != InvalidOid) { tp = SearchSysCache1(RELOID, ObjectIdGetDatum(entry->entry.rrelid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "Invalid rreloid"); namecpy(&(entry->names.rrelname), &(((Form_pg_class) GETSTRUCT(tp))->relname)); ReleaseSysCache(tp); tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(entry->entry.rrelid), entry->entry.rattnum); if (!HeapTupleIsValid(tp)) elog(ERROR, "Invalid rattr"); namecpy(&(entry->names.rattname), &(((Form_pg_attribute) GETSTRUCT(tp))->attname)); ReleaseSysCache(tp); } } /* * Request rows and buffers instrumentation if pgqs is enabled */ static void pgqs_ExecutorStart(QueryDesc *queryDesc, int eflags) { /* Setup instrumentation */ if (pgqs_enabled) { /* * For rate sampling, randomly choose top-level statement. Either all * nested statements will be explained or none will. */ if (nesting_level == 0 #if PG_VERSION_NUM >= 90600 && (!IsParallelWorker()) #endif ) { query_is_sampled = (random() <= (MAX_RANDOM_VALUE * pgqs_sample_rate)); #if PG_VERSION_NUM >= 90600 pgqs_set_query_sampled(query_is_sampled); #endif } if (pgqs_is_query_sampled()) queryDesc->instrument_options |= PGQS_FLAGS; } if (prev_ExecutorStart) prev_ExecutorStart(queryDesc, eflags); else standard_ExecutorStart(queryDesc, eflags); } /* * ExecutorRun hook: all we need do is track nesting depth */ static void pgqs_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, #if PG_VERSION_NUM >= 90600 uint64 count #else long count #endif #if PG_VERSION_NUM >= 100000 ,bool execute_once #endif ) { nesting_level++; PG_TRY(); { if (prev_ExecutorRun) #if PG_VERSION_NUM >= 100000 prev_ExecutorRun(queryDesc, direction, count, execute_once); #else prev_ExecutorRun(queryDesc, direction, count); #endif else #if PG_VERSION_NUM >= 100000 standard_ExecutorRun(queryDesc, direction, count, execute_once); #else standard_ExecutorRun(queryDesc, direction, count); #endif nesting_level--; } PG_CATCH(); { nesting_level--; PG_RE_THROW(); } PG_END_TRY(); } /* * ExecutorFinish hook: all we need do is track nesting depth */ static void pgqs_ExecutorFinish(QueryDesc *queryDesc) { nesting_level++; PG_TRY(); { if (prev_ExecutorFinish) prev_ExecutorFinish(queryDesc); else standard_ExecutorFinish(queryDesc); nesting_level--; } PG_CATCH(); { nesting_level--; PG_RE_THROW(); } PG_END_TRY(); } /* * Save a non normalized query for the queryid if no one already exists, and * do all the stat collecting job */ static void pgqs_ExecutorEnd(QueryDesc *queryDesc) { pgqsQueryStringHashKey queryKey; pgqsQueryStringEntry *queryEntry; bool found; if (pgqs_enabled && pgqs_is_query_sampled() #if PG_VERSION_NUM >= 90600 && (!IsParallelWorker()) #endif /* * multiple ExecutorStart/ExecutorEnd can be interleaved, so when sampling * is activated there's no guarantee that pgqs_is_query_sampled() will * only detect queries that were actually sampled (thus having the * required instrumentation set up). To avoid such cases, we double check * that we have the required instrumentation set up. That won't exactly * detect the sampled queries, but that should be close enough and avoid * adding to much complexity. */ && (queryDesc->instrument_options & PGQS_FLAGS) == PGQS_FLAGS ) { HASHCTL info; pgqsEntry *localentry; pgqsEntry *newEntry; HASH_SEQ_STATUS local_hash_seq; pgqsWalkerContext *context = palloc(sizeof(pgqsWalkerContext)); context->queryId = queryDesc->plannedstmt->queryId; context->rtable = queryDesc->plannedstmt->rtable; context->count = 0; context->qualid = 0; context->uniquequalid = 0; context->nbfiltered = 0; context->evaltype = 0; context->nentries = 0; context->querytext = queryDesc->sourceText; queryKey.queryid = context->queryId; /* keep an unormalized query example for each queryid if needed */ if (pgqs_track_constants) { /* Lookup the hash table entry with a shared lock. */ LWLockAcquire(pgqs->querylock, LW_SHARED); queryEntry = (pgqsQueryStringEntry *) hash_search_with_hash_value(pgqs_query_examples_hash, &queryKey, context->queryId, HASH_FIND, &found); /* Create the new entry if not present */ if (!found) { bool excl_found; /* Need exclusive lock to add a new hashtable entry - promote */ LWLockRelease(pgqs->querylock); LWLockAcquire(pgqs->querylock, LW_EXCLUSIVE); while (hash_get_num_entries(pgqs_query_examples_hash) >= pgqs_max) pgqs_queryentry_dealloc(); queryEntry = (pgqsQueryStringEntry *) hash_search_with_hash_value(pgqs_query_examples_hash, &queryKey, context->queryId, HASH_ENTER, &excl_found); /* Make sure it wasn't added by another backend */ if (!excl_found) strncpy(queryEntry->querytext, context->querytext, pgqs_query_size); } LWLockRelease(pgqs->querylock); } /* create local hash table if it hasn't been created yet */ if (!pgqs_localhash) { memset(&info, 0, sizeof(info)); info.keysize = sizeof(pgqsHashKey); if (pgqs_resolve_oids) info.entrysize = sizeof(pgqsEntryWithNames); else info.entrysize = sizeof(pgqsEntry); info.hash = pgqs_hash_fn; pgqs_localhash = hash_create("pgqs_localhash", 50, &info, HASH_ELEM | HASH_FUNCTION); } /* retrieve quals informations, main work starts from here */ pgqs_collectNodeStats(queryDesc->planstate, NIL, context); /* if any quals found, store them in shared memory */ if (context->nentries) { /* * Before acquiring exlusive lwlock, check if there's enough room * to store local hash. Also, do not remove more than 20% of * maximum number of entries in shared memory (wether they are * used or not). This should not happen since we shouldn't store * that much entries in localhash in the first place. */ int nvictims = hash_get_num_entries(pgqs_localhash) - PGQS_MAX_LOCAL_ENTRIES; if (nvictims > 0) pgqs_localentry_dealloc(nvictims); LWLockAcquire(pgqs->lock, LW_EXCLUSIVE); while (hash_get_num_entries(pgqs_hash) + hash_get_num_entries(pgqs_localhash) >= pgqs_max) pgqs_entry_dealloc(); hash_seq_init(&local_hash_seq, pgqs_localhash); while ((localentry = hash_seq_search(&local_hash_seq)) != NULL) { newEntry = (pgqsEntry *) hash_search(pgqs_hash, &localentry->key, HASH_ENTER, &found); if (!found) { /* raw copy the local entry */ memcpy(&(newEntry->lrelid), &(localentry->lrelid), sizeof(pgqsEntry) - sizeof(pgqsHashKey)); } else { /* only update counters value */ newEntry->count += localentry->count; newEntry->nbfiltered += localentry->nbfiltered; newEntry->usage += localentry->usage; newEntry->occurences += localentry->occurences; } /* cleanup local hash */ hash_search(pgqs_localhash, &localentry->key, HASH_REMOVE, NULL); } LWLockRelease(pgqs->lock); } } if (prev_ExecutorEnd) prev_ExecutorEnd(queryDesc); else standard_ExecutorEnd(queryDesc); } /* * qsort comparator for sorting into increasing usage order */ static int entry_cmp(const void *lhs, const void *rhs) { double l_usage = (*(pgqsEntry *const *) lhs)->usage; double r_usage = (*(pgqsEntry *const *) rhs)->usage; if (l_usage < r_usage) return -1; else if (l_usage > r_usage) return +1; else return 0; } /* * Deallocate least used entries. * Caller must hold an exlusive lock on pgqs->lock */ static void pgqs_entry_dealloc(void) { HASH_SEQ_STATUS hash_seq; pgqsEntry **entries; pgqsEntry *entry; int nvictims; int i; int base_size; /* * Sort entries by usage and deallocate PGQS_USAGE_DEALLOC_PERCENT of * them. While we're scanning the table, apply the decay factor to the * usage values. */ if (pgqs_resolve_oids) base_size = sizeof(pgqsEntryWithNames *); else base_size = sizeof(pgqsEntry *); entries = palloc(hash_get_num_entries(pgqs_hash) * base_size); i = 0; hash_seq_init(&hash_seq, pgqs_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { entries[i++] = entry; entry->usage *= 0.99; } qsort(entries, i, base_size, entry_cmp); nvictims = Max(10, i * PGQS_USAGE_DEALLOC_PERCENT / 100); nvictims = Min(nvictims, i); for (i = 0; i < nvictims; i++) { hash_search(pgqs_hash, &entries[i]->key, HASH_REMOVE, NULL); } pfree(entries); } /* * Deallocate the first example query. * Caller must hold an exlusive lock on pgqs->querylock */ static void pgqs_queryentry_dealloc(void) { HASH_SEQ_STATUS hash_seq; pgqsQueryStringEntry *entry; hash_seq_init(&hash_seq, pgqs_query_examples_hash); entry = hash_seq_search(&hash_seq); if (entry != NULL) hash_search_with_hash_value(pgqs_query_examples_hash, &entry->key, entry->key.queryid, HASH_REMOVE, NULL); hash_seq_term(&hash_seq); } /* * Remove the requested number of entries from pgqs_localhash. Since the * entries are all coming from the same query, remove them without any specific * sort. */ static void pgqs_localentry_dealloc(int nvictims) { pgqsEntry *localentry; HASH_SEQ_STATUS local_hash_seq; pgqsHashKey **victims; bool need_seq_term = true; int i, ptr = 0; if (nvictims <= 0) return; victims = palloc(sizeof(pgqsHashKey *) * nvictims); hash_seq_init(&local_hash_seq, pgqs_localhash); while (nvictims-- >= 0) { localentry = hash_seq_search(&local_hash_seq); /* check if caller required too many victims */ if (!localentry) { need_seq_term = false; break; } victims[ptr++] = &localentry->key; } if (need_seq_term) hash_seq_term(&local_hash_seq); for (i = 0; i < ptr; i++) hash_search(pgqs_localhash, victims[i], HASH_REMOVE, NULL); pfree(victims); } static void pgqs_collectNodeStats(PlanState *planstate, List *ancestors, pgqsWalkerContext *context) { Plan *plan = planstate->plan; int64 oldcount = context->count; double oldfiltered = context->nbfiltered; double total_filtered = 0; ListCell *lc; List *parent = 0; List *indexquals = 0; List *quals = 0; context->planstate = planstate; /* * We have to forcibly clean up the instrumentation state because we * haven't done ExecutorEnd yet. This is pretty grotty ... */ if (planstate->instrument) InstrEndLoop(planstate->instrument); switch (nodeTag(plan)) { case T_IndexOnlyScan: indexquals = ((IndexOnlyScan *) plan)->indexqual; quals = plan->qual; break; case T_IndexScan: indexquals = ((IndexScan *) plan)->indexqualorig; quals = plan->qual; break; case T_BitmapIndexScan: indexquals = ((BitmapIndexScan *) plan)->indexqualorig; quals = plan->qual; break; case T_CteScan: case T_SeqScan: case T_BitmapHeapScan: case T_TidScan: case T_SubqueryScan: case T_FunctionScan: case T_ValuesScan: case T_WorkTableScan: case T_ForeignScan: case T_ModifyTable: quals = plan->qual; break; case T_NestLoop: quals = ((NestLoop *) plan)->join.joinqual; break; case T_MergeJoin: quals = ((MergeJoin *) plan)->mergeclauses; break; case T_HashJoin: quals = ((HashJoin *) plan)->hashclauses; break; default: break; } pgqs_set_planstates(planstate, context); parent = list_union(indexquals, quals); if (list_length(parent) > 1) { context->uniquequalid = hashExpr((Expr *) parent, context, true); context->qualid = hashExpr((Expr *) parent, context, false); } total_filtered = planstate->instrument->nfiltered1 + planstate->instrument->nfiltered2; context->nbfiltered = planstate->instrument->nfiltered1 + planstate->instrument->nfiltered2; context->count = planstate->instrument->tuplecount + planstate->instrument->ntuples + total_filtered; /* Add the indexquals */ context->evaltype = 'i'; expression_tree_walker((Node *) indexquals, pgqs_whereclause_tree_walker, context); /* Add the generic quals */ context->evaltype = 'f'; expression_tree_walker((Node *) quals, pgqs_whereclause_tree_walker, context); context->qualid = 0; context->uniquequalid = 0; context->count = oldcount; context->nbfiltered = oldfiltered; foreach(lc, planstate->initPlan) { SubPlanState *sps = (SubPlanState *) lfirst(lc); pgqs_collectNodeStats(sps->planstate, ancestors, context); } /* lefttree */ if (outerPlanState(planstate)) pgqs_collectNodeStats(outerPlanState(planstate), ancestors, context); /* righttree */ if (innerPlanState(planstate)) pgqs_collectNodeStats(innerPlanState(planstate), ancestors, context); /* special child plans */ switch (nodeTag(plan)) { case T_ModifyTable: pgqs_collectMemberNodeStats(((ModifyTableState *) planstate)->mt_nplans, ((ModifyTableState *) planstate)->mt_plans, ancestors, context); break; case T_Append: pgqs_collectMemberNodeStats(((AppendState *) planstate)->as_nplans, ((AppendState *) planstate)->appendplans, ancestors, context); break; case T_MergeAppend: pgqs_collectMemberNodeStats(((MergeAppendState *) planstate)->ms_nplans, ((MergeAppendState *) planstate)->mergeplans, ancestors, context); break; case T_BitmapAnd: pgqs_collectMemberNodeStats(((BitmapAndState *) planstate)->nplans, ((BitmapAndState *) planstate)->bitmapplans, ancestors, context); break; case T_BitmapOr: pgqs_collectMemberNodeStats(((BitmapOrState *) planstate)->nplans, ((BitmapOrState *) planstate)->bitmapplans, ancestors, context); break; case T_SubqueryScan: pgqs_collectNodeStats(((SubqueryScanState *) planstate)->subplan, ancestors, context); break; default: break; } /* subPlan-s */ if (planstate->subPlan) pgqs_collectSubPlanStats(planstate->subPlan, ancestors, context); } static void pgqs_collectMemberNodeStats(int nplans, PlanState **planstates, List *ancestors, pgqsWalkerContext *context) { int j; for (j = 0; j < nplans; j++) pgqs_collectNodeStats(planstates[j], ancestors, context); } static void pgqs_collectSubPlanStats(List *plans, List *ancestors, pgqsWalkerContext *context) { ListCell *lst; foreach(lst, plans) { SubPlanState *sps = (SubPlanState *) lfirst(lst); pgqs_collectNodeStats(sps->planstate, ancestors, context); } } static pgqsEntry * pgqs_process_scalararrayopexpr(ScalarArrayOpExpr *expr, pgqsWalkerContext *context) { OpExpr *op = makeNode(OpExpr); int len = 0; pgqsEntry *entry = NULL; Expr *array = lsecond(expr->args); op->opno = expr->opno; op->opfuncid = expr->opfuncid; op->inputcollid = expr->inputcollid; op->opresulttype = BOOLOID; op->args = expr->args; switch (array->type) { case T_ArrayExpr: len = list_length(((ArrayExpr *) array)->elements); break; case T_Const: /* Const is an array. */ { Const *arrayconst = (Const *) array; ArrayType *array_type; if (arrayconst->constisnull) return NULL; array_type = DatumGetArrayTypeP(arrayconst->constvalue); if (ARR_NDIM(array_type) > 0) len = ARR_DIMS(array_type)[0]; } break; default: break; } if (len > 0) { context->count *= len; entry = pgqs_process_opexpr(op, context); } return entry; } static pgqsEntry * pgqs_process_booltest(BooleanTest *expr, pgqsWalkerContext *context) { pgqsHashKey key; pgqsEntry *entry; bool found; Var *var; Expr *newexpr = NULL; char *constant; Oid opoid; RangeTblEntry *rte; /* do not store more than 20% of possible entries in shared mem */ if (context->nentries >= PGQS_MAX_LOCAL_ENTRIES) return NULL; if (IsA(expr->arg, Var)) newexpr = pgqs_resolve_var((Var *) expr->arg, context); if (!(newexpr && IsA(newexpr, Var))) return NULL; var = (Var *) newexpr; rte = list_nth(context->rtable, var->varno - 1); switch (expr->booltesttype) { case IS_TRUE: constant = "TRUE::bool"; opoid = BooleanEqualOperator; break; case IS_FALSE: constant = "FALSE::bool"; opoid = BooleanEqualOperator; break; case IS_NOT_TRUE: constant = "TRUE::bool"; opoid = BooleanNotEqualOperator; break; case IS_NOT_FALSE: constant = "FALSE::bool"; opoid = BooleanNotEqualOperator; break; case IS_UNKNOWN: constant = "NULL::bool"; opoid = BooleanEqualOperator; break; case IS_NOT_UNKNOWN: constant = "NULL::bool"; opoid = BooleanNotEqualOperator; break; default: /* Bail out */ return NULL; } memset(&key, 0, sizeof(pgqsHashKey)); key.userid = GetUserId(); key.dbid = MyDatabaseId; key.uniquequalid = context->uniquequalid; key.uniquequalnodeid = hashExpr((Expr *) expr, context, pgqs_track_constants); key.queryid = context->queryId; key.evaltype = context->evaltype; /* local hash, no lock needed */ entry = (pgqsEntry *) hash_search(pgqs_localhash, &key, HASH_ENTER, &found); if (!found) { context->nentries++; entry->count = 0; entry->nbfiltered = 0; entry->usage = 0; entry->occurences = 0; entry->position = 0; entry->qualnodeid = hashExpr((Expr *) expr, context, false); entry->qualid = context->qualid; entry->opoid = opoid; entry->lrelid = InvalidOid; entry->lattnum = InvalidAttrNumber; entry->rrelid = InvalidOid; entry->rattnum = InvalidAttrNumber; if (rte->rtekind == RTE_RELATION) { entry->lrelid = rte->relid; entry->lattnum = var->varattno; } if (pgqs_track_constants) { char *utf8const = (char *) pg_do_encoding_conversion((unsigned char *) constant, strlen(constant), GetDatabaseEncoding(), PG_UTF8); strncpy(entry->constvalue, utf8const, strlen(utf8const)); } else memset(entry->constvalue, 0, sizeof(char) * PGQS_CONSTANT_SIZE); if (pgqs_resolve_oids) pgqs_fillnames((pgqsEntryWithNames *) entry); } entry->nbfiltered += context->nbfiltered; entry->count += context->count; entry->usage += 1; entry->occurences += 1; return entry; } static void get_const_expr(Const *constval, StringInfo buf) { Oid typoutput; bool typIsVarlena; char *extval; if (constval->constisnull) { /* * Always label the type of a NULL constant to prevent misdecisions * about type when reparsing. */ appendStringInfoString(buf, "NULL"); appendStringInfo(buf, "::%s", format_type_with_typemod(constval->consttype, constval->consttypmod)); return; } getTypeOutputInfo(constval->consttype, &typoutput, &typIsVarlena); extval = OidOutputFunctionCall(typoutput, constval->constvalue); switch (constval->consttype) { case INT2OID: case INT4OID: case INT8OID: case OIDOID: case FLOAT4OID: case FLOAT8OID: case NUMERICOID: { /* * These types are printed without quotes unless they contain * values that aren't accepted by the scanner unquoted (e.g., * 'NaN'). Note that strtod() and friends might accept NaN, * so we can't use that to test. * * In reality we only need to defend against infinity and NaN, * so we need not get too crazy about pattern matching here. * * There is a special-case gotcha: if the constant is signed, * we need to parenthesize it, else the parser might see a * leading plus/minus as binding less tightly than adjacent * operators --- particularly, the cast that we might attach * below. */ if (strspn(extval, "0123456789+-eE.") == strlen(extval)) { if (extval[0] == '+' || extval[0] == '-') appendStringInfo(buf, "(%s)", extval); else appendStringInfoString(buf, extval); } else appendStringInfo(buf, "'%s'", extval); } break; case BITOID: case VARBITOID: appendStringInfo(buf, "B'%s'", extval); break; case BOOLOID: if (strcmp(extval, "t") == 0) appendStringInfoString(buf, "true"); else appendStringInfoString(buf, "false"); break; default: appendStringInfoString(buf, quote_literal_cstr(extval)); break; } pfree(extval); /* * For showtype == 0, append ::typename unless the constant will be * implicitly typed as the right type when it is read in. */ appendStringInfo(buf, "::%s", format_type_with_typemod(constval->consttype, constval->consttypmod)); } static pgqsEntry * pgqs_process_opexpr(OpExpr *expr, pgqsWalkerContext *context) { /* do not store more than 20% of possible entries in shared mem */ if (context->nentries >= PGQS_MAX_LOCAL_ENTRIES) return NULL; if (list_length(expr->args) == 2) { Node *node = linitial(expr->args); Var *var = NULL; Const *constant = NULL; bool found; Oid *sreliddest = NULL; AttrNumber *sattnumdest = NULL; int position = -1; StringInfo buf = makeStringInfo(); pgqsHashKey key; pgqsEntry tempentry; tempentry.opoid = expr->opno; tempentry.lattnum = InvalidAttrNumber; tempentry.lrelid = InvalidOid; tempentry.rattnum = InvalidAttrNumber; tempentry.rrelid = InvalidOid; memset(&key, 0, sizeof(pgqsHashKey)); key.userid = GetUserId(); key.dbid = MyDatabaseId; key.uniquequalid = context->uniquequalid; key.uniquequalnodeid = hashExpr((Expr *) expr, context, pgqs_track_constants); key.queryid = context->queryId; key.evaltype = context->evaltype; if (IsA(node, RelabelType)) node = (Node *) ((RelabelType *) node)->arg; if (IsA(node, Var)) node = (Node *) pgqs_resolve_var((Var *) node, context); switch (node->type) { case T_Var: var = (Var *) node; { RangeTblEntry *rte; rte = list_nth(context->rtable, var->varno - 1); if (rte->rtekind == RTE_RELATION) { tempentry.lrelid = rte->relid; tempentry.lattnum = var->varattno; } } break; case T_Const: constant = (Const *) node; break; default: break; } /* If the operator can be commuted, look at it */ if (var == NULL) { if (OidIsValid(get_commutator(expr->opno))) { OpExpr *temp = copyObject(expr); CommuteOpExpr(temp); node = linitial(temp->args); sreliddest = &(tempentry.lrelid); sattnumdest = &(tempentry.lattnum); } } else { node = lsecond(expr->args); sreliddest = &(tempentry.rrelid); sattnumdest = &(tempentry.rattnum); } if (IsA(node, Var)) node = (Node *) pgqs_resolve_var((Var *) node, context); switch (node->type) { case T_Var: var = (Var *) node; { RangeTblEntry *rte = list_nth(context->rtable, var->varno - 1); *sreliddest = rte->relid; *sattnumdest = var->varattno; } break; case T_Const: constant = (Const *) node; break; default: break; } if (var != NULL) { pgqsEntry *entry; /* * If we don't track rels in the pg_catalog schema, lookup the * schema to make sure its not pg_catalog. Otherwise, bail out. */ if (!pgqs_track_pgcatalog) { HeapTuple tp; if (tempentry.lrelid != InvalidOid) { tp = SearchSysCache1(RELOID, ObjectIdGetDatum(tempentry.lrelid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "Invalid reloid"); if (((Form_pg_class) GETSTRUCT(tp))->relnamespace == PG_CATALOG_NAMESPACE) { ReleaseSysCache(tp); return NULL; } ReleaseSysCache(tp); } if (tempentry.rrelid != InvalidOid) { tp = SearchSysCache1(RELOID, ObjectIdGetDatum(tempentry.rrelid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "Invalid reloid"); if (((Form_pg_class) GETSTRUCT(tp))->relnamespace == PG_CATALOG_NAMESPACE) { ReleaseSysCache(tp); return NULL; } ReleaseSysCache(tp); } } if (constant != NULL && pgqs_track_constants) { get_const_expr(constant, buf); position = constant->location; } /* local hash, no lock needed */ entry = (pgqsEntry *) hash_search(pgqs_localhash, &key, HASH_ENTER, &found); if (!found) { char *utf8const; int len; context->nentries++; memcpy(&(entry->lrelid), &(tempentry.lrelid), sizeof(pgqsEntry) - sizeof(pgqsHashKey)); entry->count = 0; entry->nbfiltered = 0; entry->usage = 0; entry->occurences = 0; entry->position = position; entry->qualnodeid = hashExpr((Expr *) expr, context, false); entry->qualid = context->qualid; utf8const = (char *) pg_do_encoding_conversion((unsigned char *) buf->data, strlen(buf->data), GetDatabaseEncoding(), PG_UTF8); len = strlen(utf8const); /* * The const value can use multibyte characters, so we need to * be careful when truncating the value. Note that we need to * use PG_UTF8 encoding explicitly here, as the value was just * converted to this encoding. */ len = pg_encoding_mbcliplen(PG_UTF8, utf8const, len, PGQS_CONSTANT_SIZE - 1); memcpy(entry->constvalue, utf8const, len); entry->constvalue[len] = '\0'; if (pgqs_resolve_oids) pgqs_fillnames((pgqsEntryWithNames *) entry); } entry->nbfiltered += context->nbfiltered; entry->count += context->count; entry->usage += 1; entry->occurences += 1; return entry; } } return NULL; } static bool pgqs_whereclause_tree_walker(Node *node, pgqsWalkerContext *context) { if (node == NULL) return false; switch (node->type) { case T_BoolExpr: { BoolExpr *boolexpr = (BoolExpr *) node; if (boolexpr->boolop == NOT_EXPR) { /* Skip, and do not keep track of the qual */ uint32 previous_hash = context->qualid; uint32 previous_uniquequalnodeid = context->uniquequalid; context->qualid = 0; context->uniquequalid = 0; expression_tree_walker((Node *) boolexpr->args, pgqs_whereclause_tree_walker, context); context->qualid = previous_hash; context->uniquequalid = previous_uniquequalnodeid; return false; } if (boolexpr->boolop == OR_EXPR) { context->qualid = 0; context->uniquequalid = 0; } if (boolexpr->boolop == AND_EXPR) { context->uniquequalid = hashExpr((Expr *) boolexpr, context, pgqs_track_constants); context->qualid = hashExpr((Expr *) boolexpr, context, false); } expression_tree_walker((Node *) boolexpr->args, pgqs_whereclause_tree_walker, context); return false; } case T_OpExpr: pgqs_process_opexpr((OpExpr *) node, context); return false; case T_ScalarArrayOpExpr: pgqs_process_scalararrayopexpr((ScalarArrayOpExpr *) node, context); return false; case T_BooleanTest: pgqs_process_booltest((BooleanTest *) node, context); return false; default: expression_tree_walker(node, pgqs_whereclause_tree_walker, context); return false; } } static void pgqs_shmem_startup(void) { HASHCTL info; HASHCTL queryinfo; bool found; if (prev_shmem_startup_hook) prev_shmem_startup_hook(); pgqs = NULL; LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); pgqs = ShmemInitStruct("pg_qualstats", (sizeof(pgqsSharedState) #if PG_VERSION_NUM >= 90600 + pgqs_sampled_array_size() #endif ), &found); memset(&info, 0, sizeof(info)); memset(&queryinfo, 0, sizeof(queryinfo)); info.keysize = sizeof(pgqsHashKey); queryinfo.keysize = sizeof(pgqsQueryStringHashKey); queryinfo.entrysize = sizeof(pgqsQueryStringEntry) + pgqs_query_size * sizeof(char); if (pgqs_resolve_oids) info.entrysize = sizeof(pgqsEntryWithNames); else info.entrysize = sizeof(pgqsEntry); info.hash = pgqs_hash_fn; if (!found) { /* First time through ... */ #if PG_VERSION_NUM >= 90600 LWLockPadded *locks = GetNamedLWLockTranche("pg_qualstats"); pgqs->lock = &(locks[0]).lock; pgqs->querylock = &(locks[1]).lock; pgqs->sampledlock = &(locks[2]).lock; /* mark all backends as not sampled */ memset(pgqs->sampled, 0, pgqs_sampled_array_size()); #else pgqs->lock = LWLockAssign(); pgqs->querylock = LWLockAssign(); #endif } #if PG_VERSION_NUM < 90500 queryinfo.hash = pgqs_uint32_hashfn; #endif pgqs_hash = ShmemInitHash("pg_qualstatements_hash", pgqs_max, pgqs_max, &info, HASH_ELEM | HASH_FUNCTION | HASH_FIXED_SIZE); pgqs_query_examples_hash = ShmemInitHash("pg_qualqueryexamples_hash", pgqs_max, pgqs_max, &queryinfo, /* On PG > 9.5, use the HASH_BLOBS optimization for uint32 keys. */ #if PG_VERSION_NUM >= 90500 HASH_ELEM | HASH_BLOBS | HASH_FIXED_SIZE); #else HASH_ELEM | HASH_FUNCTION | HASH_FIXED_SIZE); #endif LWLockRelease(AddinShmemInitLock); } Datum pg_qualstats_reset(PG_FUNCTION_ARGS) { HASH_SEQ_STATUS hash_seq; pgqsEntry *entry; if (!pgqs || !pgqs_hash) { ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("pg_qualstats must be loaded via shared_preload_libraries"))); } LWLockAcquire(pgqs->lock, LW_EXCLUSIVE); hash_seq_init(&hash_seq, pgqs_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { hash_search(pgqs_hash, &entry->key, HASH_REMOVE, NULL); } LWLockRelease(pgqs->lock); PG_RETURN_VOID(); } Datum pg_qualstats_names(PG_FUNCTION_ARGS) { return pg_qualstats_common(fcinfo, true); } Datum pg_qualstats(PG_FUNCTION_ARGS) { return pg_qualstats_common(fcinfo, false); } Datum pg_qualstats_common(PG_FUNCTION_ARGS, bool include_names) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; int nb_columns = PGQS_COLUMNS; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; HASH_SEQ_STATUS hash_seq; pgqsEntry *entry; Datum *values; bool *nulls; if (!pgqs || !pgqs_hash) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("pg_qualstats must be loaded via shared_preload_libraries"))); /* check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not " \ "allowed in this context"))); per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; LWLockAcquire(pgqs->lock, LW_SHARED); hash_seq_init(&hash_seq, pgqs_hash); if (include_names) nb_columns += PGQS_NAME_COLUMNS; Assert(nb_columns == tupdesc->natts); values = palloc0(sizeof(Datum) * nb_columns); nulls = palloc0(sizeof(bool) * nb_columns);; while ((entry = hash_seq_search(&hash_seq)) != NULL) { int i = 0; memset(values, 0, sizeof(Datum) * nb_columns); memset(nulls, 0, sizeof(bool) * nb_columns); values[i++] = ObjectIdGetDatum(entry->key.userid); values[i++] = ObjectIdGetDatum(entry->key.dbid); if (entry->lattnum != InvalidAttrNumber) { values[i++] = ObjectIdGetDatum(entry->lrelid); values[i++] = Int16GetDatum(entry->lattnum); } else { nulls[i++] = true; nulls[i++] = true; } values[i++] = Int32GetDatum(entry->opoid); if (entry->rattnum != InvalidAttrNumber) { values[i++] = ObjectIdGetDatum(entry->rrelid); values[i++] = Int16GetDatum(entry->rattnum); } else { nulls[i++] = true; nulls[i++] = true; } if (entry->qualid == 0) nulls[i++] = true; else values[i++] = Int64GetDatum(entry->qualid); if (entry->key.uniquequalid == 0) nulls[i++] = true; else values[i++] = Int64GetDatum(entry->key.uniquequalid); values[i++] = Int64GetDatum(entry->qualnodeid); values[i++] = Int64GetDatum(entry->key.uniquequalnodeid); values[i++] = Int64GetDatum(entry->occurences); values[i++] = Int64GetDatum(entry->count); values[i++] = Int64GetDatum(entry->nbfiltered); if (entry->position == -1) nulls[i++] = true; else values[i++] = Int32GetDatum(entry->position); if (entry->key.queryid == 0) nulls[i++] = true; else values[i++] = Int64GetDatum(entry->key.queryid); if (entry->constvalue[0] != '\0') { values[i++] = CStringGetTextDatum((char *) pg_do_encoding_conversion( (unsigned char *) entry->constvalue, strlen(entry->constvalue), PG_UTF8, GetDatabaseEncoding())); } else nulls[i++] = true; if (entry->key.evaltype) values[i++] = CharGetDatum(entry->key.evaltype); else nulls[i++] = true; if (include_names) { if (pgqs_resolve_oids) { pgqsNames names = ((pgqsEntryWithNames *) entry)->names; values[i++] = CStringGetTextDatum(NameStr(names.rolname)); values[i++] = CStringGetTextDatum(NameStr(names.datname)); values[i++] = CStringGetTextDatum(NameStr(names.lrelname)); values[i++] = CStringGetTextDatum(NameStr(names.lattname)); values[i++] = CStringGetTextDatum(NameStr(names.opname)); values[i++] = CStringGetTextDatum(NameStr(names.rrelname)); values[i++] = CStringGetTextDatum(NameStr(names.rattname)); } else { for (; i < nb_columns; i++) nulls[i] = true; } } Assert(i == nb_columns); tuplestore_putvalues(tupstore, tupdesc, values, nulls); } LWLockRelease(pgqs->lock); tuplestore_donestoring(tupstore); MemoryContextSwitchTo(oldcontext); return (Datum) 0; } Datum pg_qualstats_example_query(PG_FUNCTION_ARGS) { #if PG_VERSION_NUM >= 110000 pgqs_queryid queryid = PG_GETARG_INT64(0); #else pgqs_queryid queryid = PG_GETARG_UINT32(0); #endif pgqsQueryStringEntry *entry; pgqsQueryStringHashKey queryKey; bool found; /* don't search the hash table if track_constants isn't enabled */ if (!pgqs_track_constants) PG_RETURN_NULL(); queryKey.queryid = queryid; LWLockAcquire(pgqs->querylock, LW_SHARED); entry = hash_search_with_hash_value(pgqs_query_examples_hash, &queryKey, queryid, HASH_FIND, &found); LWLockRelease(pgqs->querylock); if (found) PG_RETURN_TEXT_P(cstring_to_text(entry->querytext)); else PG_RETURN_NULL(); } Datum pg_qualstats_example_queries(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; HASH_SEQ_STATUS hash_seq; pgqsQueryStringEntry *entry; if (!pgqs || !pgqs_query_examples_hash) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("pg_qualstats must be loaded via shared_preload_libraries"))); /* check to see if caller supports us returning a tuplestore */ if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context that cannot accept a set"))); if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not " \ "allowed in this context"))); per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); tupstore = tuplestore_begin_heap(true, false, work_mem); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; MemoryContextSwitchTo(oldcontext); /* don't need to scan the hash table if track_constants isn't enabled */ if (!pgqs_track_constants) return (Datum) 0; LWLockAcquire(pgqs->querylock, LW_SHARED); hash_seq_init(&hash_seq, pgqs_query_examples_hash); while ((entry = hash_seq_search(&hash_seq)) != NULL) { Datum values[2]; bool nulls[2]; int64 queryid = entry->key.queryid; memset(values, 0, sizeof(values)); memset(nulls, 0, sizeof(nulls)); values[0] = Int64GetDatumFast(queryid); values[1] = CStringGetTextDatum(entry->querytext); tuplestore_putvalues(tupstore, tupdesc, values, nulls); } LWLockRelease(pgqs->querylock); return (Datum) 0; } /* * Calculate hash value for a key */ static uint32 pgqs_hash_fn(const void *key, Size keysize) { const pgqsHashKey *k = (const pgqsHashKey *) key; return hash_uint32((uint32) k->userid) ^ hash_uint32((uint32) k->dbid) ^ hash_uint32((uint32) k->queryid) ^ hash_uint32((uint32) k->uniquequalnodeid) ^ hash_uint32((uint32) k->uniquequalid) ^ hash_uint32((uint32) k->evaltype); } static void pgqs_set_planstates(PlanState *planstate, pgqsWalkerContext *context) { context->outer_tlist = NIL; context->inner_tlist = NIL; context->index_tlist = NIL; context->outer_planstate = NULL; context->inner_planstate = NULL; context->planstate = planstate; if (IsA(planstate, AppendState)) context->outer_planstate = ((AppendState *) planstate)->appendplans[0]; else if (IsA(planstate, MergeAppendState)) context->outer_planstate = ((MergeAppendState *) planstate)->mergeplans[0]; else if (IsA(planstate, ModifyTableState)) context->outer_planstate = ((ModifyTableState *) planstate)->mt_plans[0]; else context->outer_planstate = outerPlanState(planstate); if (context->outer_planstate) context->outer_tlist = context->outer_planstate->plan->targetlist; else context->outer_tlist = NIL; if (IsA(planstate, SubqueryScanState)) context->inner_planstate = ((SubqueryScanState *) planstate)->subplan; else if (IsA(planstate, CteScanState)) context->inner_planstate = ((CteScanState *) planstate)->cteplanstate; else context->inner_planstate = innerPlanState(planstate); if (context->inner_planstate) context->inner_tlist = context->inner_planstate->plan->targetlist; else context->inner_tlist = NIL; /* index_tlist is set only if it's an IndexOnlyScan */ if (IsA(planstate->plan, IndexOnlyScan)) context->index_tlist = ((IndexOnlyScan *) planstate->plan)->indextlist; #if PG_VERSION_NUM >= 90500 else if (IsA(planstate->plan, ForeignScan)) context->index_tlist = ((ForeignScan *) planstate->plan)->fdw_scan_tlist; else if (IsA(planstate->plan, CustomScan)) context->index_tlist = ((CustomScan *) planstate->plan)->custom_scan_tlist; #endif else context->index_tlist = NIL; } static Expr * pgqs_resolve_var(Var *var, pgqsWalkerContext *context) { List *tlist = NULL; PlanState *planstate = context->planstate; pgqs_set_planstates(context->planstate, context); switch (var->varno) { case INNER_VAR: tlist = context->inner_tlist; break; case OUTER_VAR: tlist = context->outer_tlist; break; case INDEX_VAR: tlist = context->index_tlist; break; default: return (Expr *) var; } if (tlist != NULL) { TargetEntry *entry = get_tle_by_resno(tlist, var->varattno); if (entry != NULL) { Var *newvar = (Var *) (entry->expr); if (var->varno == OUTER_VAR) pgqs_set_planstates(context->outer_planstate, context); if (var->varno == INNER_VAR) pgqs_set_planstates(context->inner_planstate, context); var = (Var *) pgqs_resolve_var(newvar, context); } } Assert(!(IsA(var, Var) && IS_SPECIAL_VARNO(var->varno))); /* If the result is something OTHER than a var, replace it by a constexpr */ if (!IsA(var, Var)) { Const *consttext; consttext = (Const *) makeConst(TEXTOID, -1, -1, -1, CStringGetTextDatum(nodeToString(var)), false, false); var = (Var *) consttext; } pgqs_set_planstates(planstate, context); return (Expr *) var; } /* * Estimate shared memory space needed. */ static Size pgqs_memsize(void) { Size size; size = MAXALIGN(sizeof(pgqsSharedState)); if (pgqs_resolve_oids) size = add_size(size, hash_estimate_size(pgqs_max, sizeof(pgqsEntryWithNames))); else size = add_size(size, hash_estimate_size(pgqs_max, sizeof(pgqsEntry))); if (pgqs_track_constants) { /* * In that case, we also need an additional struct for storing * non-normalized queries. */ size = add_size(size, hash_estimate_size(pgqs_max, sizeof(pgqsQueryStringEntry) + pgqs_query_size * sizeof(char))); } #if PG_VERSION_NUM >= 90600 size = add_size(size, MAXALIGN(pgqs_sampled_array_size())); #endif return size; } #if PG_VERSION_NUM >= 90600 static Size pgqs_sampled_array_size(void) { /* * Parallel workers need to be sampled if their original query is also * sampled. We store in shared mem the sample state for each query, * identified by their BackendId. If need room for all possible backends, plus * autovacuum launcher and workers, plus bg workers and an extra one since * BackendId numerotation starts at 1. */ return (sizeof(bool) * (MaxConnections + autovacuum_max_workers + 1 + max_worker_processes + 1)); } #endif static uint32 hashExpr(Expr *expr, pgqsWalkerContext *context, bool include_const) { StringInfo buffer = makeStringInfo(); exprRepr(expr, buffer, context, include_const); return hash_any((unsigned char *) buffer->data, buffer->len); } static void exprRepr(Expr *expr, StringInfo buffer, pgqsWalkerContext *context, bool include_const) { ListCell *lc; if (expr == NULL) return; appendStringInfo(buffer, "%d-", expr->type); if (IsA(expr, Var)) expr = pgqs_resolve_var((Var *) expr, context); switch (expr->type) { case T_List: foreach(lc, (List *) expr) exprRepr((Expr *) lfirst(lc), buffer, context, include_const); break; case T_OpExpr: appendStringInfo(buffer, "%d", ((OpExpr *) expr)->opno); exprRepr((Expr *) ((OpExpr *) expr)->args, buffer, context, include_const); break; case T_Var: { Var *var = (Var *) expr; RangeTblEntry *rte = list_nth(context->rtable, var->varno - 1); if (rte->rtekind == RTE_RELATION) appendStringInfo(buffer, "%d;%d", rte->relid, var->varattno); else appendStringInfo(buffer, "NORTE%d;%d", var->varno, var->varattno); } break; case T_BoolExpr: appendStringInfo(buffer, "%d", ((BoolExpr *) expr)->boolop); exprRepr((Expr *) ((BoolExpr *) expr)->args, buffer, context, include_const); break; case T_BooleanTest: if (include_const) appendStringInfo(buffer, "%d", ((BooleanTest *) expr)->booltesttype); exprRepr((Expr *) ((BooleanTest *) expr)->arg, buffer, context, include_const); break; case T_Const: if (include_const) get_const_expr((Const *) expr, buffer); else appendStringInfoChar(buffer, '?'); break; case T_CoerceViaIO: exprRepr((Expr *) ((CoerceViaIO *) expr)->arg, buffer, context, include_const); appendStringInfo(buffer, "|%d", ((CoerceViaIO *) expr)->resulttype); break; case T_FuncExpr: appendStringInfo(buffer, "|%d(", ((FuncExpr *) expr)->funcid); exprRepr((Expr *) ((FuncExpr *) expr)->args, buffer, context, include_const); appendStringInfoString(buffer, ")"); break; case T_MinMaxExpr: appendStringInfo(buffer, "|minmax%d(", ((MinMaxExpr *) expr)->op); exprRepr((Expr *) ((MinMaxExpr *) expr)->args, buffer, context, include_const); appendStringInfoString(buffer, ")"); break; default: appendStringInfoString(buffer, nodeToString(expr)); } } #if PG_VERSION_NUM < 90500 static uint32 pgqs_uint32_hashfn(const void *key, Size keysize) { return ((pgqsQueryStringHashKey *) key)->queryid; } #endif pg_qualstats-1.0.9/pg_qualstats.control000066400000000000000000000002211353412551700203420ustar00rootroot00000000000000comment = 'An extension collecting statistics about quals' default_version = '1.0.9' module_pathname = '$libdir/pg_qualstats' relocatable = true pg_qualstats-1.0.9/test/000077500000000000000000000000001353412551700152155ustar00rootroot00000000000000pg_qualstats-1.0.9/test/sql/000077500000000000000000000000001353412551700160145ustar00rootroot00000000000000pg_qualstats-1.0.9/test/sql/pg_qualstats.sql000066400000000000000000000010421353412551700212410ustar00rootroot00000000000000CREATE EXTENSION pg_qualstats; -- Make sure sure we'll see at least one qual SET pg_qualstats.sample_rate = 1; CREATE TABLE pgqs AS SELECT id FROM generate_series(1, 100) id; SELECT COUNT(*) FROM pgqs WHERE id = 1; SELECT lrelid::regclass::text, lattnum, occurences, execution_count, nbfiltered, constvalue, eval_type FROM pg_qualstats; SELECT COUNT(*) > 0 FROM pg_qualstats; SELECT COUNT(*) > 0 FROM pg_qualstats(); SELECT COUNT(*) > 0 FROM pg_qualstats_example_queries(); SELECT pg_qualstats_reset(); SELECT COUNT(*) FROM pg_qualstats();