0

I am running PG10 (soon to move to PG13), and I am having performance trouble with a particular query. I have also tested in PG13 and get similar results. Most of the joins in this larger query are selected for Nested Joins, but it seems a Hash Join would be more appropriate for most.

I see that the first nodes joined might be driving the statistics of later joins, where row estimates are quite low. The first joins are as follows: Dalibo Explain Visualization https://explain.dalibo.com/plan/b5045eb7cffdf0dh

SELECT * from workflowable_resource where sample_type_id in (SELECT resource_id from resource where name = 'Test Order')

While this smaller query is quite fast, I see many examples where Nested Loop is selected incorrectly where a Hash Join might perform better. (Row estimate is 1, or close to 1, but actual rows are orders of magnitude greater)

What are some general techniques for improving row estimates in these cases? I've tried increasing default_statistics_target to 10000 and analyzing all tables again, but I did not see any improvement on row estimates.

In the larger query, on the scaled up database, there is one node taking all the time. Nested Loop Index Scan There is an Index scan on the table, but it is performed in a Nested Loop as the left hand side is estimated at 1 row, but at this point is running with 178,000 rows. It seems like errors from previous row estimates are being carried forward.

9
  • 2
    Dalibo is better at looking pretty than at actually being useful. Please use explain.depesz.com instead. Or at least the raw input should put into Dalibo as text format EXPLAIN, not json format EXPLAIN. With the text format, I could at least switch to the Raw tab and then see what is going on, while with json format both the Plan tab and the Raw tab are pretty much useless. Also, you didn't include a link for the larger query, the one with the meaningful problem, you just had an image.
    – jjanes
    Commented Oct 2 at 17:35
  • explain.depesz.com/s/udum here is the Depesz explain. I will see if I can share the larger query. Commented Oct 2 at 18:44
  • 1
    Offtopic: soon to move to PG13 Why only to version 13 when you can safely migrate to version 16? That would give you a couple of years of additional support Commented Oct 3 at 13:44
  • 1
    This query has over 60 joins. Optimizing the database configuration is essential for predictable query plans and performance. Could you share the changes you have made from the default configuration? Commented Oct 4 at 13:48
  • 1
    Changing work_mem will not change anything; all sorting is already done in memory. Looking at this query, I think you have to rewrite it and make it (much) simpler. You might also need some additional indexes. Besides that, you should take a look at the configuration for all the costs. It's these costs that make or break the query plan Commented Oct 4 at 16:02

2 Answers 2

1

For the first query, it doesn't know which particular resource_id is associated with the of name of 'Test Order' until after the planning is over with. So it cannot use that specific value to estimate the number of rows from workflowable_resource it will find. So instead it has to use the generic estimate, which basically is going to be #number of rows in workflowable_resource / n_distinct for sample_type_id. There isn't much you can do about this with that query, no amount of additional statistics is going to help you as the estimate is generic anyway (unless n_distinct is way off). You could break it into two queries, plugging the output of one into the parameter of the other. But you have to do this manually, there is no way to instruct PostgreSQL to execute the inner query and then replan the outer one based on the results. Also, it likely doesn't matter, as I see no reason to think a better estimate would result in a better plan.

For the larger query, maybe a better estimate would lead to a better plan, but the information provided is inadequate to speculate. We have neither the actual plan nor the actual query.

1
  • Thanks for the insight here. I'll see if I can share the larger query and plan, but it is a long series of Nested Loops, and I think this type of limitation is recurring. We have several tables which are quite large so the Generic estimate might be quite far from reality. The distribution of rows over n_distinct is not uniform. Some sample_type_ids might have 1 row, some sample_type_ids might have 100, 1000, 10000. I will try to get some more information to share in the Question, but this is helpful. Commented Oct 2 at 18:38
1

The problem with your much larger query is rather different than with the first one, so I am adding a 2nd answer to discuss this one.

The first place where the selectivity is massively off is here:

->  Hash Join  (cost=542.37..2099.91 rows=1 width=12) (actual time=0.340..20.327 rows=6576 loops=1)                                                                                                         
  ->  Seq Scan on three_echo kilo_five  (cost=0.00..1406.89 rows=57389 width=8) (actual time=0.020..8.036 rows=57395 loops=1)
  ->  Hash  (cost=540.67..540.67 rows=136 width=4) (actual time=0.111..0.121 rows=3 loops=1)

So it thinks that of the 60,000 rows from the seq scan, only one will match to any row from the hash table. But instead, 1/10 of them find a partner in the hash table. Even though the hash table is like 40 times smaller than it thinks it will be. So I am guessing the filter applied when building the hash table selectively pulls out rows which have values which are very common the seq-scanned table (so this part actually is rather similar to the problem in the first query). Maybe you could write a much simpler query which embodies this hash join and nothing else, see of teh selectivity problem still exists, and then explore the pg_stats entries for the relevant columns of the relevant tables. Maybe you could share the unobfuscated version of that more focused query, as the obfuscation really makes it hard to reason about what is happening.

However, I'm not sure this row estimate problem is really the main culprit in the slow performance.

If the look at the plan-row which actually takes all the time:

-> Index Scan using oscar_mike on echo_four kilo_golf (cost=0.42..1,427.16 rows=11 width=12) (actual time=2.775..3.445 rows=4 loops=178,773)
   Index Cond: (kilo_golf.delta_two = alpha_xray_charlie2.four_kilo)
   Buffers: shared hit=93,849,709 dirtied=1

This index actually returns 4*178773 rows. Why does it take 93849709/4/178773 = 131 buffers hits for every row being returned? That is like 60 times more buffer accesses than what seems reasonable to me. I guess the index is full of dead tuples which have not yet been removed, or maybe it just horribly fragmented or something. I would manually VACUUM VERBOSE this table and see if that fixed (or substantially improved) the issue. If not, then I would REINDEX this specific index and see if that did it. If neither of them work, then please share with us the output of the VACUUM VERBOSE.

The blog post you reference in the comment seems a bit garbled to me, but in any case doesn't really seem relevant to you. His example has 13 buffers hits for each loop (1,299,990/100,000) and each loop returns 10 row (on average, rounded to the nearest integer). 13 is exactly what I would expect, as it usually takes 3 buffers accesses to descend the index, and then 1 buffer access for each of the 10 rows (assuming the rows are randomly scattered), 3+10=13.

You on the other hand are getting 525 buffer access per loop, which is on a whole other scale. Using the same logic as before, 3 to descend the index and one for each of 4 tuples, I would predict only 7 buffer accesses per loop, nearly 100 fold less than you are actually seeing.

Does looping over the same Shared Buffer page many times indicate a possible performance problem?

Absolutely. Finding and pinning a buffer which is already in cache is a CPU-intensive operation. It requires juggling a lot of book keeping data, and doing so under locks or atomic operations (to prevent concurrent backends from corrupting each other). That locking activity makes things slow, even when the locks are not actually contested. Now, this is no where near as slow as actually reading data from disk would be. But among non-IO operations, it is very much a heavy-weight. And, if you do it 60 times more often than is reasonable (for some still-mysterious reason), yeah it is going to cause problems.

9
  • I will give some of the above operations a try. We had performed a VACUUM ANALYZE on all tables just before this query. The Index Scan there is the most time consuming and concerning - this is our step_instance_sample table shown above. It shows 720GB of buffers scanned in those 93MM pages, which seems strange. (sis.sample_id = child_parents_connection_2.resource_id) It is being joined back to 178,700 rows in a nested loop (all preceding joins), but had planned for only 1 row. The SIS table was 190,000 n_live_tup. Before vacuum 13,284 n_dead_tup there. btree index on both sides. Int type Commented Oct 5 at 0:52
  • Neither side of the join is a primary key or unique. Step instance sample identifies samples in an experimental step. child_parents_connection_2 (an alias for resource_dependency) defines connections between 2 samples). sample_id is not unique in either step_instance_sample or resource_dependency. The sample_id is a unique primary key in another table - workflowable_resource. workflowable resource has a 1 to many relationship to both step_instance_sample as well as resource_dependency. This might not be enough, but providing it in hope it can give some context. Commented Oct 5 at 1:01
  • After upgrading to PG13, running vacuum analyze, REINDEX (concurrently) the step_instance_sample table, the production query is down from 10 minutes to 50 seconds. For the scope of the report and the join strategies used, this makes more sense (despite the still long run time). The updated query plan (obsfuscated) is here explain.depesz.com/s/TIGb The remaining performance bottlenecks are more obvious. The query targets too many rows (full system scan), and uses wildcard filters on varchar fields. The grouping of those results (array_agg) is also expensive. Commented Oct 7 at 16:29
  • For the Vacuum in PG13: INFO: vacuuming "public.step_instance_sample", INFO: scanned index "idx_sis_sample" to remove 2580 row versions, INFO: "step_instance_sample": removed 2580 row versions in 1236 pages. INFO: index "step_instance_sample_pkey" now contains 192408 row versions in 812 pages INFO: index "idx_sis_sample" now contains 192408 row versions in 222 pages DETAIL: 0 index row versions were removed. Commented Oct 7 at 16:34
  • Still not quite sure why we had to hit 720 GB of buffers for that Index Scan on btree in PG10. The step_instance_sample table is certainly not even that large. The query plan in PG13 is quite different as CTEs can be planned (non-materialized default) with the rest of the query which certainly could change the outcome. Commented Oct 7 at 16:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.