Introducing Query Tuning Workbooks to safely tune Postgres queries on production with pganalyze!

Setting up the Collector Workflow

Query Tuning is in Beta

This functionality may still change without notice. Let us know your feedback!

The Collector Workflow in Query Tuning workbooks optionally allows users with the Tune Queries permission to execute arbitrary EXPLAIN ANALYZE commands directly from the pganalyze user interface. This eliminates the need for manual copying and pasting of command outputs. These EXPLAIN ANALYZE commands are executed through the pganalyze collector.

In order to set up the Collector Workflow you must create the special pganalyze_explain role on your database server, as well as the pganalyze.explain_analyze helper function in each database where this feature should be active.

We recommend reading about Security & Privacy before setting up the Collector Workflow.

Creating the pganalyze_explain role

As an additional layer of security, we require creating a separate role in Postgres that is used to execute EXPLAIN ANALYZE statements from pganalyze. This role should not have any password assigned (and not allow login), instead it is only utilized by calling a special helper function owned by the role.

First, start by creating the new role:

CREATE ROLE pganalyze_explain;

Then, either use GRANT statements for individual tables (described separately), or assign the pg_read_all_data role to the pganalyze_explain role:

GRANT pg_read_all_data TO pganalyze_explain;

The special pg_read_all_data role lets the pganalyze_explain user read the data of all tables on the database server.

Note that access to the role is restricted through use of a helper function, which only returns the results of the EXPLAIN ANALYZE command, not the original table data, with some exceptions.

Setting up the pganalyze.explain_analyze helper

In each database where you would like to enable the Collector Workflow, you need to create the following helper function. This needs to be executed as the pganalyze_explain user, to ensure the SECURITY DEFINER privilege is correctly assigned.

First, ensure the pganalyze schema exists, temporarily allow the pganalyze_explain user to create functions in it, and switch roles:

CREATE SCHEMA IF NOT EXISTS pganalyze;
GRANT USAGE ON SCHEMA pganalyze TO pganalyze;
GRANT CREATE ON SCHEMA pganalyze TO pganalyze_explain;
SET ROLE pganalyze_explain;

Then, create the function:

CREATE OR REPLACE FUNCTION pganalyze.explain_analyze(query text, params text[], param_types text[], analyze_flags text[]) RETURNS text AS $$
DECLARE
  prepared_query text;
  params_str text;
  param_types_str text;
  explain_prefix text;
  explain_flag text;
  result text;
BEGIN
  SET TRANSACTION READ ONLY;

  PERFORM 1 FROM pg_roles WHERE (rolname = current_user AND rolsuper) OR (pg_has_role(oid, 'MEMBER') AND rolname IN ('rds_superuser', 'azure_pg_admin', 'cloudsqlsuperuser'));
  IF FOUND THEN
    RAISE EXCEPTION 'cannot run: pganalyze.explain_analyze helper is owned by superuser - recreate function with lesser privileged user';
  END IF;

  SELECT pg_catalog.regexp_replace(query, ';+s*Z', '') INTO prepared_query;
  IF prepared_query LIKE '%;%' THEN
    RAISE EXCEPTION 'cannot run pganalyze.explain_analyze helper with a multi-statement query';
  END IF;

  explain_prefix := 'EXPLAIN (VERBOSE, FORMAT JSON';
  FOR explain_flag IN SELECT * FROM unnest(analyze_flags)
  LOOP
    IF explain_flag NOT SIMILAR TO '[A-z_ ]+' THEN
      RAISE EXCEPTION 'cannot run pganalyze.explain_analyze helper with invalid flag';
    END IF;
    explain_prefix := explain_prefix || ', ' || explain_flag;
  END LOOP;
  explain_prefix := explain_prefix || ') ';

  SELECT COALESCE('(' || pg_catalog.string_agg(pg_catalog.quote_literal(p), ',') || ')', '') FROM pg_catalog.unnest(params) _(p) INTO params_str;
  SELECT COALESCE('(' || pg_catalog.string_agg(pg_catalog.quote_ident(p), ',') || ')', '') FROM pg_catalog.unnest(param_types) _(p) INTO param_types_str;

  EXECUTE 'PREPARE pganalyze_explain_analyze ' || param_types_str || ' AS ' || prepared_query;
  BEGIN
    EXECUTE explain_prefix || 'EXECUTE pganalyze_explain_analyze' || params_str INTO STRICT result;
  EXCEPTION WHEN QUERY_CANCELED OR OTHERS THEN
    DEALLOCATE pganalyze_explain_analyze;
    RAISE;
  END;
  DEALLOCATE pganalyze_explain_analyze;

  RETURN result;
END
$$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;

To complete the setup, reset the role, and revoke the creation privilege again:

RESET ROLE;
REVOKE CREATE ON SCHEMA pganalyze FROM pganalyze_explain;

Certain functions in Postgres can be problematic if permitted to be called. It is your responsibility to block use of these functions from the pganalyze_explain user.

An example of this is dblink, which, if set up on a given database, can allow copying data to a remote Postgres server, potentially letting someone with access to the pganalyze_explain user exfiltrate data.

By default dblink can be used by any user in the system, whether they are superuser or not.

As a general best practice, and to avoid granting access to the pganalyze_explain user, we recommend explicitly revoking the use of dblink from PUBLIC:

REVOKE ALL ON FUNCTION dblink(text, text) FROM PUBLIC;
REVOKE ALL ON FUNCTION dblink(text, text, bool) FROM PUBLIC;
REVOKE ALL ON FUNCTION dblink_connect(text) FROM PUBLIC;
REVOKE ALL ON FUNCTION dblink_connect(text, text) FROM PUBLIC;
REVOKE ALL ON FUNCTION dblink_exec(text, text) FROM PUBLIC;
REVOKE ALL ON FUNCTION dblink_exec(text, text, bool) FROM PUBLIC;

Note that if you have any non-superuser users utilizing dblink, you will need to explicitly grant them access to these functions instead.

Individual GRANT statements instead of pg_read_all_data

Whilst requiring more setup effort, you can also utilize GRANT to allow access to individual tables (or even individual columns), instead of using pg_read_all_data as described earlier.

For example, to grant access to all columns in table test, and only some columns in table restricted, you can run the following GRANT statements:

GRANT SELECT ON test TO pganalyze_explain;
GRANT SELECT (col1, col2) ON restricted TO pganalyze_explain;

Or alternatively you can grant access to all tables in a schema:

GRANT SELECT ON ALL TABLES IN SCHEMA myschema TO pganalyze_explain;

Note this will not grant privileges to tables in the schema that are newly created after the GRANT statement was executed.

For most systems we recommend utilizing the pg_read_all_data role to avoid having to managing individual GRANT statements.

Deactivating the Collector Workflow

In order to deactivate the collector workflow, you will first need to drop the helper function on each database:

DROP FUNCTION pganalyze.explain_analyze(query text, params text[], param_types text[], analyze_flags text[]);

And then you can optionally drop the role as well:

DROP ROLE pganalyze_explain;

Couldn't find what you were looking for or want to talk about something specific?
Start a conversation with us →