Thread: Analyze results in more expensive query plan
Hello,
We have several select statements whose performance is greatly improved by deleting some stats from pg_statistic. With the stats present the database reaches 100% cpu at 13k queries per second. Without these stats, the same machine can handle over 29k queries per second. We were able replicate this behavior with just a single join that all these queries contain. When the stats are present the planner chooses to hash join, and without stats perform a nested loop. The plan using a hash join has a higher estimated cost, and as previously mentioned, uses more cpu.
The two tables involved in this query are described below; bag_type and bag. There are 6 bag_type rows and around 6 million bag rows. During this simplified scenario, no writes were occurring. Under normal circumstances rows can be inserted into bag, and no rows in these tables are updated or deleted.
\d bag_type
Table "public.bag_type"
Column | Type | Collation | Nullable | Default
-----------+---------+-----------+----------+--------------------------------------
id | bigint | | not null | nextval('bag_type_id_seq'::regclass)
name | text | | not null |
has_slots | boolean | | not null |
game | text | | not null |
Indexes:
"bag_type_pk" PRIMARY KEY, btree (id)
"bag_name_u1" UNIQUE CONSTRAINT, btree (name, game)
Referenced by:
TABLE "bag" CONSTRAINT "bag_fk1" FOREIGN KEY (bag_type_id) REFERENCES bag_type(id)
\d bag
Table "public.bag"
Column | Type | Collation | Nullable | Default
-------------+--------+-----------+----------+---------------------------------
id | bigint | | not null | nextval('bag_id_seq'::regclass)
owner_id | uuid | | not null |
bag_type_id | bigint | | not null |
Indexes:
"bag_pk" PRIMARY KEY, btree (id)
"bag_owner_type_u1" UNIQUE CONSTRAINT, btree (owner_id, bag_type_id)
Foreign-key constraints:
"bag_fk1" FOREIGN KEY (bag_type_id) REFERENCES bag_type(id)
Referenced by:
TABLE "item" CONSTRAINT "item_fk1" FOREIGN KEY (bag_id) REFERENCES bag(id)
The pared down query joins the two tables.
EXPLAIN (ANALYZE, BUFFERS)
SELECT 1
FROM bag
INNER JOIN bag_type ON bag.bag_type_id = bag_type.id
WHERE owner_id = '00000000-0000-0000-0000-000000076100'
AND game = 'test_alpha'
AND name = ANY(ARRAY['item','wallet','buildingFixed']);
With stats on the bag_type table present, the planner uses a hash join. I noticed that the estimate of the index scan of bag_owner_type_u1 is too high at 8 rows. No owner can have more than 6 bags, so 8 should be logically impossible. Also, given 3 bag_types and a specific owner, there can't be more than 3 rows due to the bag_owner_type_u1 index.
ANALYZE bag_type;
https://explain.depesz.com/s/zcI (Slower, hash join)
If I remove the stats on the bag_type table, the planner estimates 1 row and uses a nested loop.
DELETE FROM pg_statistic s
USING pg_class c
WHERE c.oid = s.starelid
AND c.relname = 'bag_type';
https://explain.depesz.com/s/yBuEo (nested loop)
Below are various stats and configuration options, in case they are helpful. I've tried reindexing everything, clustering the tables and ran vacuum full as well. I've tried increasing the default statistics target (this actually made performance much worse). I’ve tested this on fresh volumes with synthetic data, as well as on replicas of prod data. I’ve also tested this on different ec2 instance types (r4.16xl and c4.8xl). In all cases the bag_type stats resulted in worse performance. I was hoping someone would be able to give advice on how to improve these queries that doesn’t involve deleting stats.
Thanks
--Jeremy
SELECT version();
version
-------------------------------------------------------------------------------------------------------------------------
PostgreSQL 10.7 (Debian 10.7-1.pgdg80+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 4.9.2-10+deb8u2) 4.9.2, 64-bit
(1 row)
SELECT relname, relpages, reltuples, relallvisible, relkind, relnatts, relhassubclass, reloptions, pg_table_size(oid) FROM pg_class WHERE relname='bag_type' OR relname = 'bag';
relname | relpages | reltuples | relallvisible | relkind | relnatts | relhassubclass | reloptions | pg_table_size
----------+----------+-------------+---------------+---------+----------+----------------+------------+---------------
bag | 44115 | 5.99964e+06 | 0 | r | 3 | f | | 361390080
bag_type | 1 | 6 | 0 | r | 4 | f | | 16384
(2 rows)
SELECT (SELECT sum(x) FROM unnest(most_common_freqs) x) frac_MCV, tablename, attname, inherited, null_frac, n_distinct, array_length(most_common_vals,1) n_mcv, array_length(histogram_bounds,1) n_hist, correlation FROM pg_stats WHERE attname='bag_type_id' AND tablename='bag' ORDER BY 1 DESC;
frac_mcv | tablename | attname | inherited | null_frac | n_distinct | n_mcv | n_hist | correlation
----------+-----------+-------------+-----------+-----------+------------+-------+--------+-------------
1 | bag | bag_type_id | f | 0 | 6 | 6 | | 0.167682
(1 row)
SELECT (SELECT sum(x) FROM unnest(most_common_freqs) x) frac_MCV, tablename, attname, inherited, null_frac, n_distinct, array_length(most_common_vals,1) n_mcv, array_length(histogram_bounds,1) n_hist, correlation FROM pg_stats WHERE attname='owner_id' AND tablename='bag' ORDER BY 1 DESC;
frac_mcv | tablename | attname | inherited | null_frac | n_distinct | n_mcv | n_hist | correlation
------------+-----------+----------+-----------+-----------+------------+-------+--------+-------------
0.00680001 | bag | owner_id | f | 0 | -0.123982 | 100 | 101 | 0.994306
(1 row)
SELECT (SELECT sum(x) FROM unnest(most_common_freqs) x) frac_MCV, tablename, attname, inherited, null_frac, n_distinct, array_length(most_common_vals,1) n_mcv, array_length(histogram_bounds,1) n_hist, correlation FROM pg_stats WHERE attname='name' AND tablename='bag_type' ORDER BY 1 DESC;
frac_mcv | tablename | attname | inherited | null_frac | n_distinct | n_mcv | n_hist | correlation
----------+-----------+---------+-----------+-----------+------------+-------+--------+-------------
| bag_type | name | f | 0 | -1 | | 6 | -0.428571
(1 row)
SELECT (SELECT sum(x) FROM unnest(most_common_freqs) x) frac_MCV, tablename, attname, inherited, null_frac, n_distinct, array_length(most_common_vals,1) n_mcv, array_length(histogram_bounds,1) n_hist, correlation FROM pg_stats WHERE attname='game' AND tablename='bag_type' ORDER BY 1 DESC;
frac_mcv | tablename | attname | inherited | null_frac | n_distinct | n_mcv | n_hist | correlation
----------+-----------+---------+-----------+-----------+------------+-------+--------+-------------
1 | bag_type | game | f | 0 | -0.166667 | 1 | | 1
(1 row)
SELECT name, current_setting(name), SOURCE
FROM pg_settings
WHERE SOURCE NOT IN ('default', 'override');
name | current_setting | source
-------------------------------------+---------------------------------------------+--------------------
application_name | psql | client
archive_command | /wal-e-shim wal-push %p | configuration file
archive_mode | on | configuration file
archive_timeout | 1min | configuration file
autovacuum | on | configuration file
autovacuum_max_workers | 6 | configuration file
autovacuum_vacuum_scale_factor | 0 | configuration file
autovacuum_vacuum_threshold | 10000 | configuration file
autovacuum_work_mem | -1 | configuration file
checkpoint_completion_target | 0.9 | configuration file
checkpoint_timeout | 30min | configuration file
checkpoint_warning | 30s | configuration file
client_encoding | SQL_ASCII | client
DateStyle | ISO, MDY | configuration file
dynamic_shared_memory_type | posix | configuration file
effective_cache_size | 42432000kB | configuration file
fsync | on | configuration file
full_page_writes | on | configuration file
huge_pages | try | configuration file
idle_in_transaction_session_timeout | 10min | configuration file
lc_messages | C | configuration file
lc_monetary | C | configuration file
lc_numeric | C | configuration file
lc_time | C | configuration file
listen_addresses | * | configuration file
log_autovacuum_min_duration | 0 | configuration file
log_checkpoints | on | configuration file
log_destination | stderr | configuration file
log_line_prefix | %t [%p-%l] %q%u@%d | configuration file
log_lock_waits | on | configuration file
log_min_duration_statement | 1s | configuration file
log_temp_files | 0 | configuration file
log_timezone | UTC | configuration file
maintenance_work_mem | 3536000kB | configuration file
max_connections | 400 | configuration file
max_prepared_transactions | 100 | configuration file
max_stack_depth | 2MB | configuration file
max_wal_senders | 5 | configuration file
max_wal_size | 34GB | configuration file
pg_partman_bgw.dbname | redacted | configuration file
pg_partman_bgw.interval | 3600 | configuration file
pg_partman_bgw.role | postgres | configuration file
pg_stat_statements.track | all | configuration file
port | 5432 | command line
random_page_cost | 1.1 | configuration file
shared_buffers | 14144000kB | configuration file
shared_preload_libraries | plpgsql, pg_partman_bgw, pg_stat_statements | configuration file
stats_temp_directory | /var/run/postgresql/pg_stat_tmp | configuration file
superuser_reserved_connections | 5 | configuration file
synchronous_commit | on | configuration file
TimeZone | UTC | configuration file
unix_socket_directories | /var/run/postgresql | configuration file
unix_socket_group | postgres | configuration file
unix_socket_permissions | 0700 | configuration file
wal_keep_segments | 64 | configuration file
wal_level | replica | configuration file
wal_sync_method | fsync | configuration file
work_mem | 141440kB | configuration file
(58 rows)
Jeremy Altavilla <jeremyaltavilla@gmail.com> writes: > We have several select statements whose performance is greatly improved by > deleting some stats from pg_statistic. You might have better results by setting up some "extended stats" for the combination of bag_type columns that this query depends on. Per your description, there's a fair amount of cross-column correlation, which the planner will not expect without some extended stats to tell it so. https://www.postgresql.org/docs/10/planner-stats.html#PLANNER-STATS-EXTENDED regards, tom lane
create statistics bag_type_stats (dependencies) on id, name, game from bag_type;
analyze bag_type;
create statistics bag_stats (dependencies) on id, owner_id, bag_type_id from bag;
analyze bag;
select * from pg_statistic_ext;
-[ RECORD 1 ]---+------------------------------------------------------------------------------------------------------------------------------------------------------
stxrelid | 16411
stxname | bag_stats
stxnamespace | 2200
stxowner | 10
stxkeys | 1 2 3
stxkind | {f}
stxndistinct |
stxdependencies | {"1 => 2": 1.000000, "1 => 3": 1.000000, "2 => 1": 0.966567, "2 => 3": 0.966567, "1, 2 => 3": 1.000000, "1, 3 => 2": 1.000000, "2, 3 => 1": 1.000000}
-[ RECORD 2 ]---+------------------------------------------------------------------------------------------------------------------------------------------------------
stxrelid | 16398
stxname | bag_type_stats
stxnamespace | 2200
stxowner | 10
stxkeys | 1 2 4
stxkind | {f}
stxndistinct |
stxdependencies | {"1 => 2": 1.000000, "1 => 4": 1.000000, "2 => 1": 1.000000, "2 => 4": 1.000000, "1, 2 => 4": 1.000000, "1, 4 => 2": 1.000000, "2, 4 => 1": 1.000000}
On Fri, May 17, 2019 at 9:35 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
>
> Jeremy Altavilla <jeremyaltavilla@gmail.com> writes:
> > We have several select statements whose performance is greatly improved by
> > deleting some stats from pg_statistic.
>
> You might have better results by setting up some "extended stats" for
> the combination of bag_type columns that this query depends on. Per your
> description, there's a fair amount of cross-column correlation, which
> the planner will not expect without some extended stats to tell it so.
>
> https://www.postgresql.org/docs/10/planner-stats.html#PLANNER-STATS-EXTENDED
>
> regards, tom lane
On Tue, 21 May 2019 at 08:23, Jeremy Altavilla <jeremyaltavilla@gmail.com> wrote: > > Thanks for the suggestion. I created extended statistics objects for the two tables in question. Unfortunately the resultingplan was the same (and had the same estimates). It looks like the extended stats discovered a potentially usefulcorrelation on bag: "2, 3 => 1" (owner_id, bag_type_id => id). I'm guessing this wasn't usable because the docs state"They are not used to improve estimates for equality conditions comparing two columns". I'd say that since the time spent planning is near 3x what is spent during execution that you're wasting your time trying to speed up the execution. What you should be thinking about is using PREPAREd statements to avoid the planning overhead completely. If that's not possible then you've more chance of reducing the time spent planning by reducing the statistics on the table rather than adding more planning overhead by adding extended stats. You might want to experiment with ALTER TABLE ... ALTER COLUMN ... SET STATISTICS ..; and setting those down a bit then analyzing the tables again. Although, that's likely only going to make a very small difference, if any, than getting rid of the planning completely. -- David Rowley http://www.2ndQuadrant.com/ PostgreSQL Development, 24x7 Support, Training & Services
Thanks for the help. In our prod environment, we shouldn't be planning unnecessarily. Our app uses the extended query protocol (prepare/bind/exec) to call pg/plsql stored procedures. I left out a lot of context and background in my question, because I hoped it simplified things. I might have left out too much though. Immediately after we upgraded our prod database from postgres 9.6 to 10, it started using 2-3x more cpu with no change in requests per second. We have a load test for this app / database; using it we eventually discovered the effect of having stats on the bag_type table. After that, It made sense that the upgrade triggered this, as a step in the upgrade process is to run analyze new cluster. I experimented with changing the per column statistics value. Setting name and game to 0, results in no stats for those columns, and the planner choosing the better plan. Pretty much any other set of values resulted in the more expensive plan. I'm not sure if this is fixing the problem, or hiding the problem, but it's definitely less fragile than hoping the table never gets analyzed. --Thanks --Jeremy On Mon, May 20, 2019 at 10:04 PM David Rowley <david.rowley@2ndquadrant.com> wrote: > > On Tue, 21 May 2019 at 08:23, Jeremy Altavilla > <jeremyaltavilla@gmail.com> wrote: > > > > Thanks for the suggestion. I created extended statistics objects for the two tables in question. Unfortunately the resultingplan was the same (and had the same estimates). It looks like the extended stats discovered a potentially usefulcorrelation on bag: "2, 3 => 1" (owner_id, bag_type_id => id). I'm guessing this wasn't usable because the docs state"They are not used to improve estimates for equality conditions comparing two columns". > > I'd say that since the time spent planning is near 3x what is spent > during execution that you're wasting your time trying to speed up the > execution. What you should be thinking about is using PREPAREd > statements to avoid the planning overhead completely. If that's not > possible then you've more chance of reducing the time spent planning > by reducing the statistics on the table rather than adding more > planning overhead by adding extended stats. You might want to > experiment with ALTER TABLE ... ALTER COLUMN ... SET STATISTICS ..; > and setting those down a bit then analyzing the tables again. > Although, that's likely only going to make a very small difference, if > any, than getting rid of the planning completely. > > -- > David Rowley http://www.2ndQuadrant.com/ > PostgreSQL Development, 24x7 Support, Training & Services