PostgreSQL 19's pg_plan_advice: Finally, We Can Tweak the Planner


PostgreSQL 19 came out yesterday. There’s one feature that’s making the rounds in DBA circles - pg_plan_advice. I’ve been wanting something like this for ages.
Look, PostgreSQL’s query planner is generally solid. But there are times when it drives you up the wall. You twiddle with random_page_cost, you mess with enable_seqscan, and yet the planner does whatever it wants anyway. Oracle has hints. SQL Server has hints. PostgreSQL? Nah, we don’t do that here. Or at least, we didn’t.
Now we do.
pg_plan_advice is a contrib module that ships with 19. You’ll need to install it separately, but that’s no big deal. The really cool part is how it works: you can either let it analyze a plan you like and generate advice from that, or you can hand-write your own. Both approaches are valid.
Just don’t come crying to me if you write terrible advice and your queries start crawling. The documentation is upfront about this - garbage in, garbage out. Play nice with it.
| |
Simple enough. Let’s look at what you can actually do with it.
Here’s a scenario: you’ve got a query that’s running perfectly. The plan is beautiful. You want to make sure it stays that way, even when statistics change and the planner gets Ideas. Here’s the trick:
| |
What comes back looks something like this:
| |
Plain English: do a sequential scan on orders, but grab the index for that customer lookup.
Now the fun part. Take that output and use it:
| |
Boom. The planner now has to follow your rules. Not its own judgment. Yours.
Let me walk through the main things you can tweak.
| |
This forces an index scan on prod_name_idx when querying products. You can also do INDEX_ONLY_SCAN, BITMAP_HEAP_SCAN, or just SEQ_SCAN if you’re feeling contrarian.
| |
That example means: join customers to orders first, then bring in line_items. You can also specify join methods directly:
| |
Want a nested loop instead? Use NESTED_LOOP_PLAIN. Merge join? MERGE_JOIN. Pick your poison.
| |
GATHER forces parallel execution. NO_GATHER disables it. For partitioned tables, there’s PARTITIONWISE to control whether partition-wise joins happen.
Picture this: you’ve got a reporting query that’s important but slow.
| |
You ran EXPLAIN. The planner keeps choosing nested loops when you know - you just know - a hash join would crush it. You’ve tried everything else.
Try this:
| |
It should listen now.
Of course there’s a catch. The docs are straightforward about limitations:
Also, this is PostgreSQL 19 only. Older versions need to wait.
Honestly? Most of the time, you don’t need this. The planner is smart. Really smart. But every now and then, you run into a situation where:
Those are the times to reach for this.
PostgreSQL has historically resisted hinting. The philosophy was: if you need to hint, something is wrong upstream. Schema design, statistics, whatever. And honestly, that’s usually correct.
But the community came around. pg_hint_plan existed as an extension for years. This is better - it’s built-in, it’s official, and it has that round-trip guarantee where advice generated from a plan will work the same way when fed back.
Is it dangerous? Sure, if you use it carelessly. But so is a chainsaw. Sometimes you just need the right tool.
Go experiment. Just maybe not on Monday morning.
Join us and experience the power of SQLFlash today!.