Using the hint table has been pretty painful, in my experience. Two main difficulties I’ve seen:
1. The hint patterns are whitespace-sensitive. Accidentally put a tab instead of a couple of spaces, and you get a silent match failure.
2. There are different ways to encode query parameters - `?` for literals in the query text, `$n` for query parameters. Psql / pgcli don’t support parameterized queries so you can’t use them to iterate on your hints.
Still super useful when you have no other options though.
It's a bit of both.
What pg_hint_plan does is change the cost to favour the hinted suggestion.
If hinted suggestion is impossible, the planner will still perform something else.
But if 'force' means changing what it would do otherwise, where 'otherwise' is: a different plan having the lowest cost, the: yes that is exactly what it does.
I’ve been doing maintenance and bug fixes for this module for over 18 months now (last commit on HEAD seems to be mine), managing the last two releases. If you have questions and/or feedback, feel free.
I am digging into postgres source code past few weeks. This project seems like a good way to see how plugins integrate. I may reach out later if I have questions
There are many Postgres internals that people are usually not aware of, with more than one way to develop your module. I have a repo that’s a set of plugin templates here, that should be handy for your studies: https://github.com/michaelpq/pg_plugins
Just wanted to say thank you! This extension was critical to a bunch of my research (and now my lab's research as well). Being able to control fine-grained elements of each plan while letting the PG planner "do the rest" has saved me personally probably 100s of hours of work.
Is there a way to bypass SQL completely, and somehow give the Postgres the plan to execute? So targeting their intermediate language (i presume, it is not stable?) instead of the front one
There are hooks for the planner and the executor, so you could force their hand quite easily with a plan extracted from a previous EXPLAIN. Being able to pass a plan at protocol level would be more efficient, for sure. I’ve worked on Postgres-XC and somewhat Postgres-XL, and that’s something that we wanted to do to generate a plan on one node, and push down the compiled plan down to other nodes to avoid the overhead of the plan computation. This stuff did that for snapshots and a few more things with an extension of the protocol.
- db will generate new plans as necessary when row counts and values change. Putting in hints makes the plan rigid likely leading to headaches down the line.
- as new Postgres comes out, it's planner will do a better job. Again forcing specific plan might force the planner into optimization that no longer is optimal.
In my experience developers almost never comeback to a query with hints to double check if hints are really needed.
Famously oracle has query hints that don't do nothing no more, that are ignored, but oracle can't remove them from query language because that would break too many existing queries.
I like Postgres stance that if query planner doesn't do a good job, then dba should first update table/column statistics, and if things are truly bad, submit but to Postgres so the query optimizer can be updated itself.
Saying all that, hints support through an extension to Postgres is a good compromise. Postgres developers don't need to bother with hints, its a third party feature. And dba/users, if they really need hints, now they have them.
What about when column statistics don’t accurately describe a subset of the table that you know from a business perspective is likely to have different dynamics? Allowing the client to essentially A/B test between different advisements and evaluate performance, with full knowledge of that business context, may be meaningful at certain scales.
That's valuable, but cranking statistics up to 10000 and/or creating custom statistics can go a very long way to helping the planner understand the dataset more fully.
Sure, but for production environments being able to tell the planner to do it the way you want (aka "fix it for right now") is often the overriding priority.
The "make it work work perfectly from just statistics" can come later, when time is more flexible. Probably on an identical non-production copy of the environment, if that shows the same performance characteristics. ;)
Can you please explain how cranking statistics 10000 and/or creating custom statistics is a more straightfoward way to enforce a certain query plan VS telling the databsee which plan should be used instead?
The goal isn't to enforce a certain query plan. The goal is to execute queries efficiently.
The planner/optimizer is good at its job. In general, better than I am. If I can inform it about a particular shape of data, all queries around that data will improve. If I tell it exactly how to execute one query, only that query improves.
Additionally, setting statistics to 10000 is really easy. Easier than tweaking a query. Creating custom statistics is more work and may or may not be worth exploring as a quick fix. But if you know the data is shaped irregularly, why wouldn't you make sure the planner knows about it?
> - db will generate new plans as necessary when row counts and values change. Putting in hints makes the plan rigid likely leading to headaches down the line.
> - as new Postgres comes out, it's planner will do a better job. Again forcing specific plan might force the planner into optimization that no longer is optimal.
It depends what is your priority. Most production environments want to favor predictability over raw performance. I'd rather trade 10% performance degradation in average for a consistent query performance.
Even if statistics or new versions could come with 10% better plans, I prefer that my query's performance is predictable and does not experience high p90s or even worse that you risk experiencing plan flips that turn your 0.2s 10K/qps query into a 40s query.
100%, even if my performance slow degrades as my data changes I would much rather have gradual performance and be able to manually switch to a better plan at some point than have the DB suddenly switch a new plan which is probably better, but if not may cause a huge performance swing that takes my application offline.
It would be very interesting if there was some sort of system that ensured query plans don't change "randomly" but if it noticed that a new plan was expected to be better on most queries it would surface this in some interface that you could then test and approve.
Maybe it could also support gradual rollout, try this on 1% of queries first, then if it does improve performance it can be fully approved.
It would be extra cool if this was automatic. New plans could slowly be slowly rolled out by the DB and rolled back if they aren't actually better.
> I like Postgres stance that if query planner doesn't do a good job, then dba should first update table/column statistics, and if things are truly bad, submit but to Postgres so the query optimizer can be updated itself.
That’s a very unhelpful stance when I’m having an incident in production because PG decided to use a stupid query plan. Waiting months - years for a bugfix (which might not even be backported to my PG version) is not a solution.
I agree that hints are a very dangerous thing, but they’re invaluable as an emergency measure
I like it as an emergency measure, but I often see them used when there's a shallow understanding of operating the db.
Before using a hint or rewriting a query to force a specific plan, I try and push the team to do these things:
1. Run `vacuum analyze` and tune the auto vacuum settings. This fixes issues surprisingly often.
2. Increase statistics on the table.
3. Tweak planner settings globally or just for the query. Stuff like `set local join_collapse_limit=1` can fix a bad plan. This is pretty similar to hinting, so not a huge argument that this is better beyond not requiring an extension.
All those methods are try and guess. With hints you can have a scientific approach to understand why the bad plan has been chosen and find the right plan. Then, you can address the root cause.
join_collapse_limit=1 may set the join order but not the join direction, so that's not enough if cardinality is misestimated. And pg_hint_plan can set this parameter for one statement if that's what you want, better than setting for the transaction
In a database supporting several applications or even just a large data model it can be rather difficult to ensure a global setting to the query planner doesn't cause a regression to other queries. A query hint can be a nice way to quickly solve a performance issue without risk of regressing elsewhere. Agree they should be used as a last resort and as a sort term fix while a better, longer term fix is investigated but they are a critical tool to have in your tool box especially as postgres moves into the business critical domains occupied by the commercial database vendors.
I have been burned before when a query on a newer db version was "optimized" in a different way that caused performance to drop but I have not yet had a query on the same version vary so drastically as to cause a problem.
Is this a more common occurrence that I just have not encountered before?
It is fairly common in e.g. multi-terabyte Postgres databases. Beyond a certain scale, it can become impossible for the statistics collector to build an accurate model for large tables. When this happens, you can end up in the strange place where the query plans change every time you run analyze even though the data model and data distribution hasn't changed. I've seen operational issues at multiple companies due to the query plans randomly changing like this on large tables in Postgres.
There are other ways to achieve that (optimizer features and decisions can be influenced by config settings, or by altering entries in statistics tables), but it's admittedly quite clunky. I agree that a production incident at 3am is definitely not the right time to start grokking these settings. The extension can be a good remedy for such cases.
More philosophically, the query planner is a finely-tuned piece of engineering. If your mental model disagrees with it ("Why isn't it using the index I want?") then it's highly likely your mental model is wrong.
Your data model might contain obvious mistakes. Statistics can be out of date too, if e.g. a table was bulk loaded and never analyzed. `ANALYZE tablename` done. Sometimes removing unused indexes can improve things. TLDR; it's always something that you need to tune in your own database. When in doubt, the query planner is right and you are wrong. Good engineering means having the intellectual curiosity to exhaust these possibility before resorting to hints.
Hints are an extreme measure. You're basically saying that you know better than the query planner, for now until eternity, and you choose to optimize by hand. That may be the case but it requires detailed knowledge of your table and access patterns. The vast majority of misbehaving query plans just need updated statistics or a better index.
Queries are complex and the query planner is using heuristics that might just not fit your situation in some cases. The query planner is great for 99.99% of queries but a super small number of edge cases will need tuning at the same time.
Finding out what went wrong in the query plan by looking at optimizer traces is a lot of work. I did so recently and the trace alone was 317MB.
This is not a correct assumption with Postgres. Its statistics collection process has fundamental flaws when tables become large such that the statistics it uses for optimization are in no way representative of the actual data, leading to the situation this thread is about.
Statistics collection is a weak spot in Postgres and query optimization relies on that information to do its job.
> you know better than the query planner, for now until eternity,
Nope, you just have to know it's fixing a real problem today.
Having a query regress in performance below a KPI would be worse than not taking advantage of a further optimization in the future, due to out of date hint.
> Good engineering means having the intellectual curiosity to exhaust these possibility before resorting to hints
Why is that better?
Luckily we don't have to rely on such grandiose claims. Just try it out. If you find a query that you can tune better than the planner for your data set, then it's a better outcome.
> Hints are an extreme measure. You're basically saying that you know better than the query planner
Absolutely.
A query planner does not analyze the complete and precise solution space, none of them do. The query planner will be extremely wrong sometimes. The only logical solution is to provide a mechanism to guide the planner towards the correct solution.
I've worked with DB2, Oracle and MS SQL Server over the last 3 decades and the story is always the same, the planner is not perfect.
Those points are academically correct. In the real world you can end up with the query planner doing very boneheaded stuff that threatens to literally break your business when there's another approach that's fast it just seems to miss. In these cases hints would be a lot easier than the stuff you end up doing.
That's the theory but it does not work this way in practice.
Many of the issues people run into with the Postgres query planner/optimizer are side effects of architectural peculiarities with no straightforward fix. Some of these issues, such as the limitations of Postgres statistics collection, have been unaddressed for decades because they are extremely difficult to address without major architectural surgery that will have its own side effects.
In my opinion, Postgres either needs to allow people to disable/bypass the query planner with hints or whatever, or commit to the difficult and unpleasant work of fixing the architecture so that these issues no longer occur. This persistent state of affairs of neither fixing the core problems nor allowing a mechanism to work around them is not user friendly. I like and use Postgres but this aspect of the project is quite broken.
The alternative is to put a disclaimer in the Postgres documentation that it should not be used for certain types of common workloads or large data models; many of these issues don't show up in force until databases become quite large.
This is a chronic source of pain in large-scale Postgres installations.
I don't know which "architectural peculiarities" you have in mind, but the issues we have are largely independent of the architecture. So I'm not sure what exactly you think we should be fixing architecture-wise ...
The way I see it the issues we have in query planning/optimization are largely independent of the overall architecture. It's simply a consequence of relying on statistics too much. But there's always going to be inaccuracies, because the whole point of stats is that it's a compact/lossy approximation of the data. No matter how much more statistics we might collect there will always be some details missing, until the statistics are way too large to be practical.
There are discussions about considering how "risky" a given plan is - some plan shapes may be more problematic, but I don't think anyone submitted any patch so far. But even with that would not be a perfect solution - no approach relying solely on a priori information can be.
IMHO the only way forward is to work on making the plans more robust to this kind of mistakes. But that's very hard too.
This is the main motivation behind learned "steering" query optimizers: even if a DBA finds the right hint for a query, it is difficult to track that hint through data changes, future DB versions, and even query changes (e.g., if you add another condition to the WHERE clause, should you keep the hints or drop them?). Turns out, with a little bit of systems work, ML models can do a reasonably good job of selecting a good hint for a particular query plan.
What is cool about Postgres extensions ecosystem, is that new functionality can be built upon existing extensions. For example, Babelfish (SQL Server compatibility layer for Postgres) uses pg_hint_plan to implement Transact-SQL hints support [1]. This is really useful, because existing queries, that were optimized for MSSQL, may require tuning when moved to Postgres/Babelfish, and it is great to have additional knobs for tuning.
Second because all the knobs available to tune the query planner (listed in your last link) are obscure and require deep understanding of Postgres internals to make sense of them, which means that only a fraction of people understand them whereas query plans are more pedestrian. They’re also often database-scoped or session-scoped rather than query-scoped, it’s a huge risk of making things worse rather than better.
Even if bugs are fixed instantly, nobody will apply a patch in production withiut previous testing. Changing system-wide behavior to fix a single query may make things worse. Hints are the only way to fix at the scope of one statement with the guarantee that it doesn't break others
Okay so it isn't entirely clear to me, can the pg_hint_plan extension (linked in the OP) do the simple thing where we specify, for each table, which index to use?
Because, the mssql WITH(INDEX()) is simple and intuitive. This hint table stuff seems complicated, and it's unclear to me if they can do the simple thing
(Note: WITH(INDEX()) is different than specifying whether you will do SeqScan or IndexScan. it's, like, in SQL we can give a name to an index or to a constraint, and one may want to be able to do IndexScan, yes, but with a specific named index)
Not related to the project but do you know of useful content that explains how to approach query optimization for Postgres? All I was able to find was classic stuff like `explain analyze` etc.
Beyond those resources, here are a few useful things I've learned:
1. `explain (analyze, buffers)` is useful. It will tell you about hot vs cold data. One caveat: it doesn't deduplicate the buffer hits, so 1M buffer hits could be only a few thousand unique pages. But I still find it useful especially when comparing query plans.
2. pg_buffercache. Knowing what's in the buffer allows you to optimize the long tail of queries that perform buffer reads. Sometimes rebuilding an index on an unrelated table can create space in the buffer for the data the query needs.
3. Try using dedicated covering partial indexes for high traffic queries. An index-only scan is super cheap and with the right include and where condition you can make it small and efficient.
The tips above are especially useful in Aurora, where the shared buffers are huge (there's no page cache so it's the only caching layer).
My biggest problem with Postgres is the Planner/Optimizer.
The worst example is a super simple query:
SELECT * FROM TABLE FOO WHERE ORGANIZATION_ID = 10 ORDER BY CREATED_AT LIMIT 10 OFFSET 0;
Postgres sorts by created_at using an index, but uses the filter on organization_id. This organization has a million rows... without the order, the query runs in ms. In order, in seconds/minutes.
Sometimes the plan changes if you change the query offset!
For organization 10, the plane is goood, for organization 11, the plane is bad...
Change stats, add more stats, fill the void. that doesn't solve anything. The solution is to use a calc on order by , but in the end you need to create an index using Organization_id and created_at to really fix it.
I did the same test on SQLServer and SQLite, they executed the query correctly, using the correct index. I've never created an index to fix a bad plan in SQLServer, and I've used SqlServer a lot more than Postgres.
And I use a Pass database, it's impossible to install extensions...
I'm going to test this extension locally, if it works I'll convince the company to use it (by changing the database provider)
In addition to what I described there, the way you described it, it sounds like you don't have a single index that covers both columns. That may also help if I understood right.
In all databases you will avoid bad plans (and the unpredictable performance related to plan changes) by providing the right index. You have two selective filters: WHERE and LIMIT so the right index have both
Maybe it is not the planner. On difference between those other databases and PostgreSQL is that their plan do not depend on how freshly the table was vacuumed. The cost of your "correct index" may becomes worse when the rows are updated until they are vacuumed.
> For organization 10, the plane is goood, for organization 11, the plane is bad...
that's probably because for 11, there will be more results (say 10k), and optimizer decided that it is faster to scan already sorted index with filter, than sort 10k rows.
Logic of optimizer is usually reasonable, my problem with it is that it relies on cost config parameters, which I think by default are inadequate for modern servers, it is not clear how to set it correctly, and actual optimizer rules are not documented, but one need to look at PgSQL code to understand them.
Abandoned? Certainly not. But it's a complex part of a mature database product, with many existing deployments/users, which means the improvement is affecting literally everyone. And query planning in general is a hard problem. So it takes time to get new stuff in.
I agree with everything you said, but it's frustrating that the planner fails on a seemingly trivial query ("seemingly trivial", perhaps it's a very complex problem).
> So it takes time to get new stuff in.
It's true, I discovered this problem in PG 9, and it's true in PG 16, if I'm not mistaken.
I think this very much depends on what exactly is the problem - if it's with how costing for LIMIT works (https://news.ycombinator.com/item?id=39728826), then yeah, this did not change since 9.x in a substantial way, so 16 has the same behavior.
On a technical level, this is hard because the optimizer has a fairly limited amount of information (optimizer stats) - in particular, it does not know if the columns are correlated. If they are not, then this costing is perfect, and the plan is the correct one. And this assumption of independence is what most databases do by default, so we do that. But if the columns are correlated, it blows up, unfortunately. That is, it's not a very robust plan :-(
But I think the really hard problem is the impact the change might have on existing systems. As I said, the problem is we have very little information to inform the decision. We could penalize this "risky" plan somehow (we don't really have any concept of "risk" separately from the cost), but inevitably that will make the query much worse for some of the systems where that plan is correct.
I understand the frustration, though.
(FWIW I love sqlite, it's an amazing piece of software, but claiming that it has a better optimizer based on a single query may be a bit ... premature.)
So, why PG devs are resistant against introducing hints, and allowing users optimize base on their knowledge about data nature and have predictable results?..
I don't think there's a uniform agreement to not have hints, and even for devs opposing the idea of hints is there is not a single universal reason and it's more like every single dev has his own reason(s) to not like them. For example:
1) The dev may be working on something else entirely, not caring about hints at all. I'd bet most devs are actually in this group.
2) PG does the right decisions for the use cases the dev needs/supports, so there's not much motivation to make this complex, or feeling hints are needed. Everyone scratches their own itch in the first place.
3) There's a feeling we shouldn't be fixing planning issues by hints but by fixing the optimizer. I agree it's rather idealistic and perhaps not very practical (how does it help that in 2 years there might be a fix, when you have the problem on PROD now?) or supported by history (there's a bunch of cases that we know are a problem for years, not much progress was made).
4) Often bad past experience with applications overusing hints to force plans that the "smart" developer thought are great, but then it grew to 100x the size over years, and now the plans are insane. But also the hints are part of the application code and that can't be changed and it's obviously he fault of the DBA to "make it work". If I had a $1 for every such application I had to deal with ...
5) There issues are fairly rare (depending on the application/schema/,...) are there are other ways to force the database to use a different plan. You can do CTEs, set different cost parameters, disable some plan nodes, ... Not as explicit as hints, but good enough for no one to spend enough time on hints.
6) Belief this can/should be solved out of core. The fact that pg_hint_plan exists suggests maybe it's not a bad idea.
7) Concerns about complexity - the optimizer is quite complicated piece of code already, hints would make it even more complex (both the code and testing). If the community is not ready to accept this extra complexity and maintain it forever, it won't be added.
8) There are proprietary/commercial forks that actually do support hints - for example EDB Postgres Advanced Server supports this. (disclosure: I work for EDB, although not on EPAS.)
9) I don't think there ever was a formal patch adding this capability. And without a patch it's all just a theoretical discussion.
10) ... probably many more reasons ...
I'm not saying any of this is a good reason to (not) support hints, it's merely my observation of what devs say when asked about hints.
imho, lack of hints makes PG just dangerous to use in prod, since DB can stuck in poorly optimized query any moment, and shutdown whole site/system. It would be priority #1 if I would be product manager of this project, and all your bullet points would be naturally resolved.
That sounds like the pg stats are messed up somehow. Try running explain analyze foo to see if it changes things.
Postgres doesn’t automatically create indexes - I bet the others have some implicit index on organization_id, created_at. Otherwise they would be giving bad results in other cases. I don’t see why you can’t just make that index in postgres.
If changing random_page_cost from 4 to 2 makes a difference, then probably there are no good indexes. The choice between Seq Scan and Index Scan should be obvious without depending on small adjustments or one day, with slightly different data distribution the plan will flip to a bad one
It's really hard to say why this is happening without EXPLAIN ANALYZE (and even then it may not be obvious), but it very much seems like the problem with correlated columns we often have with LIMIT queries.
The optimizer simply assumes the matching rows are "uniformly" distributed, because limit is costed as linear approximation of the startup/total cost of the input node. For example, consider this made up query plan
Limit (cost=0.0..100.0 rows=10)
-> Index scan (cost=0.0...10000000.0 rows=1000000)
Filter: dept_id=10
The table may have many more rows - say, 100x more. But the optimizer assumes the rows with (dept_id=10) are distributed in the index, so it can scan the first 1/100000 if the index to get the 10 rows the limits needs. So it assumes the cost is 10*(0+10000000)/1000000.
But chances are the dept_id=10 rows happen to be at the very end of the index, so the planner actually needs to scan almost the whole index, making the cost wildly incorrect.
You can verify this by looking how far the dept_id rows are in "order by created_id" results. If there are many other rows before the 10 rows you need, it's likely this.
Sadly, the optimizer is not smart enough to realize there's this risk. I agree it's an annoying robustness issue, but I don't have a good idea how to mitigate it ... I wonder what the other databases do.
I'd suggest reporting it to pgsql-performance mailing list [1], and continuing the discussion there. I'd expect more community members to join that discussion there.
One thing that I've run across is when doing a select on a large table with serial or indexed timestamps, ordered by that column is that the plans are horrible when the criteria you're using are unexpectedly rare, even if there's an index on that column. e.g. If col foo has low cardinality, with a bunch of common but a few rare entries, the planner will prefer using the ordering index to the column index even for the very low frequency ones.
Partial indexes can be really useful there, as well as order by (id+0). But it's a total hack.
I've not been able to reproduce this behavior. Maybe someone else will have better luck.
create unlogged table foo (id int primary key generated by default as identity, organization_id int default floor(random()*10), created_at timestamptz default timestamp '2010-01-01' + random()*(timestamp '2020-01-01' - timestamp '2010-01-01'));
create index on foo (created_at);
create index on foo (organization_id);
alter table foo add column updated_at timestamptz null default '2010-01-01 00:00:00'::timestamp without time zone + random() * ('2020-01-01 00:00:00'::timestamp without time zone - '2010-01-01 00:00:00'::timestamp without time zone);
create index on foo (updated_at);
insert into foo (id) select generate_series(1, 1e8);
cluster foo using foo_created_at_idx;
vacuum analyze;
explain analyze select * from foo where organization_id = 1 order by updated_at limit 10;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.57..169.50 rows=10 width=24) (actual time=0.088..0.242 rows=10 loops=1)
-> Index Scan using foo_updated_at_idx on foo (cost=0.57..173267047.88 rows=10256672 width=24) (actual time=0.086..0.237 rows=10 loops=1)
Filter: (organization_id = 1)
Rows Removed by Filter: 73
Planning Time: 0.168 ms
Execution Time: 0.269 ms
You reproduced it. See the index being used. foo_updated_at_idx, not foo_organization_id_idx. See a FILTER on Organization_id, not a Cond index. If you create an index using Organization_id and updated_at the plan changes to:
Limit (cost=0.42..3.26 rows=10 width=24) (actual time=0.013..0.024 rows=10 loops=1)
-> Index Scan using foo_organization_id_updated_at_idx on foo (cost=0.42..2011.50 rows=7063 width=24) (actual time=0.012..0.023 rows=10 loops=1)
Index Cond: (organization_id = 1)
Planning Time: 0.154 ms
Execution Time: 0.033 ms
foo_organization_id_idx is the correct index, in my case using foo_organization_at_idx is 100x, 1000x faster.
In all my queries, the correct index is in the where clause, not the order by clause: When I removed the use of the wrong index, (ORDER BY update_at + range '0 day' forces the use of the correct index), the query used less resources and was much faster.
Of course, there may be cases where the index in the order by clause is the best one to use, but in my use this never happened.
With only two indexes, one on organization_id and one on updated_at, for the query
select * from foo where organization_id = 1 order by updated_at limit 10 offset 0;
I think foo_updated_at_idx is the better choice and not foo_organization_id_idx. If it chose foo_organization_id_idx then there would be ~1e7 matching rows whose pages would have to be randomly accessed and then sorted. I'll try the same exercise in SQLite, but right now I can't see how it could possibly do better.
> When I removed the use of the wrong index, (ORDER BY update_at + range '0 day' forces the use of the correct index), the query used less resources and was much faster.
I don't know about the "range" keyword. When I try that in PostgreSQL, I get a syntax error, though the "interval" keyword works. However, that produces a much worse plan:
As expected, because of the high-cardinality (~1e7) for any given organization_id, because the pages will have to be accessed randomly, and because they all have to be sorted before the limit can be applied, there's no benefit to the using foo_organization_id_idx. Sure enough, it has to do a table scan, then a sort, before it can apply the limit, and it's about 6000x slower.
I dumped the data from postgres and loaded into a similar (same name, same columns, in the same order, same columns indexed) it works in much the same way.
explain query plan select * from foo where organization_id = 1 order by updated_at limit 10;
QUERY PLAN
|--SEARCH foo USING INDEX foo_organization_id_idx (organization_id=?)
`--USE TEMP B-TREE FOR ORDER BY
Just as with Postgres, it starts by ordering the data, though in this case it uses a TEMP B-TREE rather than the index on updated_at (not sure why it's doing that). Then, it filters on organization_id=1, though in this case it uses the index to filter on organization_id. That takes 3.8s on my machine (postgres took ~260ms for the same operation). If I drop the index on organization_id, this is the query plan
QUERY PLAN
`--SCAN foo USING INDEX foo_updated_at_idx
It's back to scanning the foo_updated_at_idx index, just like Postgres does. Interestingly, this takes mere milliseconds. Dropping the index on organization_id actually improves the performance.
Finally, just for good measure, if I also drop the foo_updated_at_idx index, this is the query plan
QUERY PLAN
|--SCAN foo
`--USE TEMP B-TREE FOR ORDER BY
Running the query in this case takes 4.2s. It's back to using a TEMP B-TREE index for ORDER BY clause, but it has to scan the table.
For me, it's hard to draw any hard-and-fast lessons from this. In my experience, database performance is highly context dependent. It depends on the volume and distribution of data, the hardware, its current load, the indexes in place, and how the query is written. These little experiments to me just serve to confirm that.
I guess the one hard-and-fast lesson I personally take from this is not to be so certain that there is "a correct query plan" and that I or really anyone knows better than the database what that is. Sometimes in certain circumstances that may be true, but usually it's not.
The sample data is distributed equally, each page contains data from all organizations.
The actual data is much more grouped by organization, like this:
OK. I dropped the data, recreated it according to the recipe, and re-ran the query. The plan is different, using the index on organization_id rather than on updated_at, but that's to be expected given that the table is clustered differently. I agree that given this new way of organizing the data, foo_organization_id_idx is a better candidate for the "best index", but that's exactly what Postgres picks. Sorry, but I continue not to see the problem.
truncate table foo; --start over
drop index foo_updated_at_idx1; --drop the partial index for organization_id = 10. don't need it.
insert into foo ( --insert the data exactly as prescribed
id,
created_at,
updated_at
)
select
generate_series(1, 4000000),
generate_series('2010-01-01 00:00:00'::timestamp, now(), ('2010-01-01 00:00:00'::timestamp - now())/4000000),
generate_series('2010-01-01 00:00:00'::timestamp, now(), ('2010-01-01 00:00:00'::timestamp - now())/4000000);
cluster foo using foo_organization_id_idx; --cluster it by organization_id exactly as prescribed
vacuum analyze; --ditto
explain analyze select * from foo where organization_id = 9 order by updated_at limit 100000 offset 0;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=30928.71..42596.19 rows=100000 width=57) (actual time=35.177..52.661 rows=100000 loops=1)
-> Gather Merge (cost=30928.71..69134.11 rows=327452 width=57) (actual time=35.173..49.672 rows=100000 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Sort (cost=29928.68..30338.00 rows=163726 width=57) (actual time=25.208..27.079 rows=33940 loops=3)
Sort Key: updated_at
Sort Method: external merge Disk: 9808kB
Worker 0: Sort Method: external merge Disk: 5112kB
Worker 1: Sort Method: external merge Disk: 5144kB
-> Parallel Index Scan using foo_organization_id_idx on foo (cost=0.43..9592.75 rows=163726 width=57) (actual time=0.065..12.548 rows=133674 loops=3)
Index Cond: (organization_id = 9)
Planning Time: 0.173 ms
Execution Time: 55.360 ms
-- 1e7 entries for organization_id = 10 distributed evenly over the 10
-- years after '2020-01-01 00:00:00' as requested
insert into foo (
organization_id,
created_at,
updated_at)
select
10, --9 was the previous max organization_id so presumably 10 is OK rather than 11
generate_series('2020-01-01 00:00:00'::timestamp, '2020-01-01 00:00:00'::timestamp + interval '10 years', (interval '10 years')/1e7),
generate_series('2020-01-01 00:00:00'::timestamp, '2020-01-01 00:00:00'::timestamp + interval '10 years', (interval '10 years')/1e7);
cluster foo using foo_created_at_idx; --should be a no-op but why not
vacuum analyze; --same thing
create index on foo (updated_at) where organization_id = 10; --stats will be highly-skewed so we had better fix that
explain analyze select * from foo where organization_id = 10 order by updated_at limit 10;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..23.35 rows=10 width=57) (actual time=0.073..0.079 rows=10 loops=1)
-> Index Scan using foo_updated_at_idx1 on foo (cost=0.43..23495662.12 rows=10254706 width=57) (actual time=0.071..0.075 rows=10 loops=1)
Planning Time: 0.185 ms
Execution Time: 0.101 ms
BTW, SQLite was no better at solving this riddle. It also failed miserably in its query plan until I created the same partial index.
I simplified the SQL a little bit - I don't think it's necessary to have two timestamps and cluster on one of them. The other timestamp is not correlated, so the access through that index will be random anyway. And I used 10M rows only.
create unlogged table foo (
id int primary key generated by default as identity,
organization_id int,
created_at timestamptz
);
insert into foo (id, organization_id, created_at)
select
i, floor(random()*10),
timestamp '2010-01-01' + random()*(timestamp '2020-01-01' - timestamp '2010-01-01')
from generate_series(1, 1e7) s(i);
insert into foo (id, organization_id, created_at)
select
1e7 + i, 10,
timestamp '2020-01-01' + random()*(timestamp '2021-01-01' - timestamp '2020-01-01')
from generate_series(1, 100000) s(i);
-- see the timestamp range for each org
select organization_id, min(created_at), max(created_at) from foo group by 1 order by 1;
create index on foo (created_at);
vacuum analyze;
checkpoint;
set max_parallel_workers_per_gather = 0;
explain analyze select * from foo where organization_id = 1 order by created_at limit 10;
explain analyze select * from foo where organization_id = 10 order by created_at limit 10;
For me, this produces:
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..5.62 rows=10 width=16) (actual time=0.072..0.378 rows=10 loops=1)
-> Index Scan using foo_created_at_idx on foo (cost=0.43..505826.61 rows=975331 width=16) (actual time=0.071..0.375 rows=10 loops=1)
Filter: (organization_id = 1)
Rows Removed by Filter: 76
Planning Time: 0.040 ms
Execution Time: 0.390 ms
(6 rows)
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..43.61 rows=10 width=16) (actual time=30316.079..30316.117 rows=10 loops=1)
-> Index Scan using foo_created_at_idx on foo (cost=0.43..505826.61 rows=117161 width=16) (actual time=30316.078..30316.114 rows=10 loops=1)
Filter: (organization_id = 10)
Rows Removed by Filter: 10000000
Planning Time: 0.041 ms
Execution Time: 30316.136 ms
(6 rows)
So perhaps there's something "wrong" with your data, because there's no "Filter" or "Rows Removed by Filter" in your query plans.
Yeah. But the whole discussion here was about the dilemma the optimizer faces if it only has the two indexes on (created_at) and (organization_id), and why the assumption of independence/uniformity does not work for the skewed case.
Of course, adding a composite or partial index may help, but people are confused why the optimizer is not smart enough to just switch to the other index, which on my machine does this:
> Yeah. But the whole discussion here was about the dilemma the optimizer faces if it only has the two indexes on (created_at) and (organization_id), and why the assumption of independence/uniformity does not work for the skewed case.
Was it? The whole discussion started when zac23or said at the top of the thread that their biggest problem with PostgreSQL is its optimizer, that this is their worst example, that it needs an index on (organization_id, created_id) for it to be solved, and that SQLite and MS SQL Server do not need that index on (organization_id, created_at). They didn't initially say how their data are distributed other than that there are millions of rows with organization_id=10 and they didn't initially say that their data are skewed. Later, they said that the data are "distributed equally" and added that organization_id is random, which would be consistent, though they did say that their data are clustered not by created_at but by organization_id. Consequently, the skewed distribution seems to be a special case that you added in this sub-thread. That's fine, but that's a different albeit related problem from the one zac23or posed. If the original problem is, "Arrange for PostgreSQL to perform well on this query on this data model for equally-distributed data without adding an index on (organization_id, created_id)." that problem is solved. It can be done. If your additional problem is "Arrange for PostgreSQL to perform well on this query on this data model for unequally-distributed data without adding an index on (organization_id, created_at)." that problem is also solved with a partial index on just (created_at).
> Of course, adding a composite or partial index may help, but people are confused why the optimizer is not smart enough to just switch to the other index, which on my machine does this:
I'm curious how you forced the optimizer to switch to that query plan if it's not smart enough to do it on its own. I don't know, but I suspect you ordered by an expression, with something like
explain analyze select * from foo where organization_id = 10 order by created_at + interval '0 day' limit 10;
That produces nearly identical results on my machine, which increases my suspicion.
This plan is better for organization_id=10 than the one the optimizer chose, without resorting to a partial index, but it comes at a cost: this plan is worse for any of the other values of organization_id, whose data are not skewed.
I think a third question, which is implicit in your comments is, "Well then why doesn't the optimizer use one plan for organization_id=10 and the other plan for the other values of organization_id?" That's a good question. Is this possible? Does any other database do this? I don't know. I'll...
>> Yeah. But the whole discussion here was about the dilemma the optimizer faces if it only has the two indexes on (created_at) and (organization_id), and why the assumption of independence/uniformity does not work for the skewed case.
>
> Was it? The whole discussion started when zac23or said at the top of the thread that their biggest problem with PostgreSQL is its optimizer, that this is their worst example, that it needs an index on (organization_id, created_id) for it to be solved, and that SQLite and MS SQL Server do not need that index on (organization_id, created_at). ...
I don't know, but that's how I understood the discussion - I tried to explain why the Postgres optimizer makes this choice, and why planning this type of queries (LIMIT + WHERE) is a hard problem in principle.
>> Of course, adding a composite or partial index may help, but people are confused why the optimizer is not smart enough to just switch to the other index, which on my machine does this:
>
> I'm curious how you forced the optimizer to switch to that query plan if it's not smart enough to do it on its own.
I simply dropped the index on created_at, forcing the planner to use the other index.
> I think a third question, which is implicit in your comments is, "Well then why doesn't the optimizer use one plan for organization_id=10 and the other plan for the other values of organization_id?" That's a good question. Is this possible? Does any other database do this? I don't know. I'll try to find out but if somebody already has the answer, I'd love to hear it.
The answer is that this does not happen because the optimizer does not have any concept of dependence between the two columns, or (perhaps more importantly) between values in these two columns. So it can't say that for some departments it's not correlated but for ID 10 it is (and should use a different plan).
Maybe it would be possible to implement some new multi-column statistics. I don't think any of the existing optional extended statistics could provide this, but maybe some form of multi-dimensional histogram could? Or something like that? But ultimately, this can never be a complete fix - there'll always be some type of cross-column dependency the statistics can't detect.
> A fourth question is, "How do other databases perform on this and similar queries?"
I'm not sure what all the other databases do, but I think handling this better will require some measure of "risk" or "plan robustness". In other words, some ability to say - how sensitive is the plan to estimates not being perfectly accurate, or maybe some other assumptions being violated. But that's gonna be "better worst case, worse average" solution.
> I don't know, but that's how I understood the discussion - I tried to explain why the Postgres optimizer makes this choice, and why planning this type of queries (LIMIT + WHERE) is a hard problem in principle.
That may be a hard problem. If it is, then a harder problem still is limit + order by. That's why I almost always tell people that limit is not what you want and probably never should be used outside of ad-hoc data exploration.
I've often wanted (or thought that I wanted) something that works similarly to a SQL database but where you write queries at about the same level of abstraction as the output of EXPLAIN. SQL is great for generating ad-hoc reports, or in other contexts where performance isn't critical. But in a production environment you can't always rely on it to perform as expected.
The planner wasn’t particularly sophisticated in those days, nor was the data volume crushing.
But in the end, we would take all of our queries, and check their plans. We would pad the db with data until the combination of data and statistics and the planner all did what we wanted.
Once we were happy with the statistics and the results we’d reset the database with “empty” data for the customer, and never touch the statistics again.
We were mostly trying to eliminate table scans on simple queries and joins, and influencing index selection.
IMO query plan hinting should be done out of band, using table source names to refer to the parts of the query. In psql this might take the form of a \command that lets one specify the hints for the preceding query.
115 comments
[ 4.0 ms ] story [ 206 ms ] threadhttps://news.ycombinator.com/item?id=39712127
Glad to see it executed
Still super useful when you have no other options though.
Isn't this what PREPARE and EXECUTE does?
For older versions, you can do:
The GitHub repo's wording is broken english currently though which is (probably?) worse.
- db will generate new plans as necessary when row counts and values change. Putting in hints makes the plan rigid likely leading to headaches down the line.
- as new Postgres comes out, it's planner will do a better job. Again forcing specific plan might force the planner into optimization that no longer is optimal.
In my experience developers almost never comeback to a query with hints to double check if hints are really needed.
Famously oracle has query hints that don't do nothing no more, that are ignored, but oracle can't remove them from query language because that would break too many existing queries.
I like Postgres stance that if query planner doesn't do a good job, then dba should first update table/column statistics, and if things are truly bad, submit but to Postgres so the query optimizer can be updated itself.
Saying all that, hints support through an extension to Postgres is a good compromise. Postgres developers don't need to bother with hints, its a third party feature. And dba/users, if they really need hints, now they have them.
The "make it work work perfectly from just statistics" can come later, when time is more flexible. Probably on an identical non-production copy of the environment, if that shows the same performance characteristics. ;)
The planner/optimizer is good at its job. In general, better than I am. If I can inform it about a particular shape of data, all queries around that data will improve. If I tell it exactly how to execute one query, only that query improves.
Additionally, setting statistics to 10000 is really easy. Easier than tweaking a query. Creating custom statistics is more work and may or may not be worth exploring as a quick fix. But if you know the data is shaped irregularly, why wouldn't you make sure the planner knows about it?
What's needed, then, is a benchmark suite that tests if those hints still give a better performance than a hintless query
> - db will generate new plans as necessary when row counts and values change. Putting in hints makes the plan rigid likely leading to headaches down the line.
> - as new Postgres comes out, it's planner will do a better job. Again forcing specific plan might force the planner into optimization that no longer is optimal.
It depends what is your priority. Most production environments want to favor predictability over raw performance. I'd rather trade 10% performance degradation in average for a consistent query performance.
Even if statistics or new versions could come with 10% better plans, I prefer that my query's performance is predictable and does not experience high p90s or even worse that you risk experiencing plan flips that turn your 0.2s 10K/qps query into a 40s query.
It would be very interesting if there was some sort of system that ensured query plans don't change "randomly" but if it noticed that a new plan was expected to be better on most queries it would surface this in some interface that you could then test and approve.
Maybe it could also support gradual rollout, try this on 1% of queries first, then if it does improve performance it can be fully approved.
It would be extra cool if this was automatic. New plans could slowly be slowly rolled out by the DB and rolled back if they aren't actually better.
That’s a very unhelpful stance when I’m having an incident in production because PG decided to use a stupid query plan. Waiting months - years for a bugfix (which might not even be backported to my PG version) is not a solution.
I agree that hints are a very dangerous thing, but they’re invaluable as an emergency measure
Before using a hint or rewriting a query to force a specific plan, I try and push the team to do these things:
1. Run `vacuum analyze` and tune the auto vacuum settings. This fixes issues surprisingly often. 2. Increase statistics on the table. 3. Tweak planner settings globally or just for the query. Stuff like `set local join_collapse_limit=1` can fix a bad plan. This is pretty similar to hinting, so not a huge argument that this is better beyond not requiring an extension.
Is this a more common occurrence that I just have not encountered before?
Your data model might contain obvious mistakes. Statistics can be out of date too, if e.g. a table was bulk loaded and never analyzed. `ANALYZE tablename` done. Sometimes removing unused indexes can improve things. TLDR; it's always something that you need to tune in your own database. When in doubt, the query planner is right and you are wrong. Good engineering means having the intellectual curiosity to exhaust these possibility before resorting to hints.
Hints are an extreme measure. You're basically saying that you know better than the query planner, for now until eternity, and you choose to optimize by hand. That may be the case but it requires detailed knowledge of your table and access patterns. The vast majority of misbehaving query plans just need updated statistics or a better index.
Finding out what went wrong in the query plan by looking at optimizer traces is a lot of work. I did so recently and the trace alone was 317MB.
Statistics collection is a weak spot in Postgres and query optimization relies on that information to do its job.
Nope, you just have to know it's fixing a real problem today.
Having a query regress in performance below a KPI would be worse than not taking advantage of a further optimization in the future, due to out of date hint.
> Good engineering means having the intellectual curiosity to exhaust these possibility before resorting to hints
Why is that better?
Luckily we don't have to rely on such grandiose claims. Just try it out. If you find a query that you can tune better than the planner for your data set, then it's a better outcome.
Yes I do know better what data is going to be in the database I am managing that some heuristic mechanism.
While it's great that "good engineering" exists, it often requires a bunch more time and effort than people have for the task right then.
Being inflexible and always demanding that time is taken is an extremely poor approach, and often doesn't lead to a good quality result.
Instead it often leads to the inflexible thing being retired and a more flexible alternative being used from then on.
Absolutely.
A query planner does not analyze the complete and precise solution space, none of them do. The query planner will be extremely wrong sometimes. The only logical solution is to provide a mechanism to guide the planner towards the correct solution.
I've worked with DB2, Oracle and MS SQL Server over the last 3 decades and the story is always the same, the planner is not perfect.
Many of the issues people run into with the Postgres query planner/optimizer are side effects of architectural peculiarities with no straightforward fix. Some of these issues, such as the limitations of Postgres statistics collection, have been unaddressed for decades because they are extremely difficult to address without major architectural surgery that will have its own side effects.
In my opinion, Postgres either needs to allow people to disable/bypass the query planner with hints or whatever, or commit to the difficult and unpleasant work of fixing the architecture so that these issues no longer occur. This persistent state of affairs of neither fixing the core problems nor allowing a mechanism to work around them is not user friendly. I like and use Postgres but this aspect of the project is quite broken.
The alternative is to put a disclaimer in the Postgres documentation that it should not be used for certain types of common workloads or large data models; many of these issues don't show up in force until databases become quite large.
This is a chronic source of pain in large-scale Postgres installations.
The way I see it the issues we have in query planning/optimization are largely independent of the overall architecture. It's simply a consequence of relying on statistics too much. But there's always going to be inaccuracies, because the whole point of stats is that it's a compact/lossy approximation of the data. No matter how much more statistics we might collect there will always be some details missing, until the statistics are way too large to be practical.
There are discussions about considering how "risky" a given plan is - some plan shapes may be more problematic, but I don't think anyone submitted any patch so far. But even with that would not be a perfect solution - no approach relying solely on a priori information can be.
IMHO the only way forward is to work on making the plans more robust to this kind of mistakes. But that's very hard too.
It is certainly a bad practice, but on very particular occasions it is invaluable.
[1] https://github.com/babelfish-for-postgresql/babelfish-for-po...
[0] https://stackoverflow.com/questions/6593765/how-to-use-index...
[1] https://stackoverflow.com/questions/309786/how-do-i-force-po... - and https://www.2ndquadrant.com/en/blog/hinting_at_postgresql/ says why Postgres devs think this is a bad feature
First because “we get bugs fixed quickly” is true but not true enough (my own experience: https://www.postgresql.org/message-id/17540-7aa1855ad5ec18b4... has not been fixed since reported 1.5y ago)
Second because all the knobs available to tune the query planner (listed in your last link) are obscure and require deep understanding of Postgres internals to make sense of them, which means that only a fraction of people understand them whereas query plans are more pedestrian. They’re also often database-scoped or session-scoped rather than query-scoped, it’s a huge risk of making things worse rather than better.
I don't just not agree, I truly don't understand how they can take that position knowing the nature of the problem.
"The problem"=optimizing over a large space in limited time without complete info. It will never be perfect, there will always be exceptions.
I can't find it here
https://github.com/ossc-db/pg_hint_plan/blob/master/docs/hin...
Because, the mssql WITH(INDEX()) is simple and intuitive. This hint table stuff seems complicated, and it's unclear to me if they can do the simple thing
(Note: WITH(INDEX()) is different than specifying whether you will do SeqScan or IndexScan. it's, like, in SQL we can give a name to an index or to a constraint, and one may want to be able to do IndexScan, yes, but with a specific named index)
Beyond those resources, here are a few useful things I've learned: 1. `explain (analyze, buffers)` is useful. It will tell you about hot vs cold data. One caveat: it doesn't deduplicate the buffer hits, so 1M buffer hits could be only a few thousand unique pages. But I still find it useful especially when comparing query plans. 2. pg_buffercache. Knowing what's in the buffer allows you to optimize the long tail of queries that perform buffer reads. Sometimes rebuilding an index on an unrelated table can create space in the buffer for the data the query needs. 3. Try using dedicated covering partial indexes for high traffic queries. An index-only scan is super cheap and with the right include and where condition you can make it small and efficient.
The tips above are especially useful in Aurora, where the shared buffers are huge (there's no page cache so it's the only caching layer).
https://explain.dalibo.com/
SELECT * FROM TABLE FOO WHERE ORGANIZATION_ID = 10 ORDER BY CREATED_AT LIMIT 10 OFFSET 0;
Postgres sorts by created_at using an index, but uses the filter on organization_id. This organization has a million rows... without the order, the query runs in ms. In order, in seconds/minutes.
Sometimes the plan changes if you change the query offset!
For organization 10, the plane is goood, for organization 11, the plane is bad...
Change stats, add more stats, fill the void. that doesn't solve anything. The solution is to use a calc on order by , but in the end you need to create an index using Organization_id and created_at to really fix it.
I did the same test on SQLServer and SQLite, they executed the query correctly, using the correct index. I've never created an index to fix a bad plan in SQLServer, and I've used SqlServer a lot more than Postgres.
And I use a Pass database, it's impossible to install extensions...
I'm going to test this extension locally, if it works I'll convince the company to use it (by changing the database provider)
In addition to what I described there, the way you described it, it sounds like you don't have a single index that covers both columns. That may also help if I understood right.
Yes. But other databases (SQLlite, etc.) select the correct index, without the need to create an index with both columns.
that's probably because for 11, there will be more results (say 10k), and optimizer decided that it is faster to scan already sorted index with filter, than sort 10k rows.
Logic of optimizer is usually reasonable, my problem with it is that it relies on cost config parameters, which I think by default are inadequate for modern servers, it is not clear how to set it correctly, and actual optimizer rules are not documented, but one need to look at PgSQL code to understand them.
I agree. My problem is that SQLite, an embedded database, seems to have a better Planner/Optimizer than Postgres.
Is the development of Planner/Optimizer abandoned?
> my problem with it is that it relies on cost config parameters
And I don't have access to change it, as it's a PASS database...
> So it takes time to get new stuff in.
It's true, I discovered this problem in PG 9, and it's true in PG 16, if I'm not mistaken.
On a technical level, this is hard because the optimizer has a fairly limited amount of information (optimizer stats) - in particular, it does not know if the columns are correlated. If they are not, then this costing is perfect, and the plan is the correct one. And this assumption of independence is what most databases do by default, so we do that. But if the columns are correlated, it blows up, unfortunately. That is, it's not a very robust plan :-(
But I think the really hard problem is the impact the change might have on existing systems. As I said, the problem is we have very little information to inform the decision. We could penalize this "risky" plan somehow (we don't really have any concept of "risk" separately from the cost), but inevitably that will make the query much worse for some of the systems where that plan is correct.
I understand the frustration, though.
(FWIW I love sqlite, it's an amazing piece of software, but claiming that it has a better optimizer based on a single query may be a bit ... premature.)
1) The dev may be working on something else entirely, not caring about hints at all. I'd bet most devs are actually in this group.
2) PG does the right decisions for the use cases the dev needs/supports, so there's not much motivation to make this complex, or feeling hints are needed. Everyone scratches their own itch in the first place.
3) There's a feeling we shouldn't be fixing planning issues by hints but by fixing the optimizer. I agree it's rather idealistic and perhaps not very practical (how does it help that in 2 years there might be a fix, when you have the problem on PROD now?) or supported by history (there's a bunch of cases that we know are a problem for years, not much progress was made).
4) Often bad past experience with applications overusing hints to force plans that the "smart" developer thought are great, but then it grew to 100x the size over years, and now the plans are insane. But also the hints are part of the application code and that can't be changed and it's obviously he fault of the DBA to "make it work". If I had a $1 for every such application I had to deal with ...
5) There issues are fairly rare (depending on the application/schema/,...) are there are other ways to force the database to use a different plan. You can do CTEs, set different cost parameters, disable some plan nodes, ... Not as explicit as hints, but good enough for no one to spend enough time on hints.
6) Belief this can/should be solved out of core. The fact that pg_hint_plan exists suggests maybe it's not a bad idea.
7) Concerns about complexity - the optimizer is quite complicated piece of code already, hints would make it even more complex (both the code and testing). If the community is not ready to accept this extra complexity and maintain it forever, it won't be added.
8) There are proprietary/commercial forks that actually do support hints - for example EDB Postgres Advanced Server supports this. (disclosure: I work for EDB, although not on EPAS.)
9) I don't think there ever was a formal patch adding this capability. And without a patch it's all just a theoretical discussion.
10) ... probably many more reasons ...
I'm not saying any of this is a good reason to (not) support hints, it's merely my observation of what devs say when asked about hints.
Postgres doesn’t automatically create indexes - I bet the others have some implicit index on organization_id, created_at. Otherwise they would be giving bad results in other cases. I don’t see why you can’t just make that index in postgres.
I did
> Postgres doesn’t automatically create indexes
Both fields have indexes (created manually). Postgres is incorrectly choosing the index in order by column, not the index in the where clause.
What are the current values of `random_page_cost` and `seq_page_cost`?
The default is typically 4, and in practice with modern disks you should use a lower value closer to 1.I have many other problems with the planner, this is the most absurd due to the simplicity of the query.
The optimizer simply assumes the matching rows are "uniformly" distributed, because limit is costed as linear approximation of the startup/total cost of the input node. For example, consider this made up query plan
The table may have many more rows - say, 100x more. But the optimizer assumes the rows with (dept_id=10) are distributed in the index, so it can scan the first 1/100000 if the index to get the 10 rows the limits needs. So it assumes the cost is 10*(0+10000000)/1000000.But chances are the dept_id=10 rows happen to be at the very end of the index, so the planner actually needs to scan almost the whole index, making the cost wildly incorrect.
You can verify this by looking how far the dept_id rows are in "order by created_id" results. If there are many other rows before the 10 rows you need, it's likely this.
Sadly, the optimizer is not smart enough to realize there's this risk. I agree it's an annoying robustness issue, but I don't have a good idea how to mitigate it ... I wonder what the other databases do.
I can run EXPLAIN and paste it here, but it's just one of the problems with the planner (in a very basic query), I have many other problems with it.
On the other hand, I've never had any other serious problems with Postgres. It's a very good database.
Might as well, could turn up something useful. :)
[1] https://www.postgresql.org/list/pgsql-performance/
Partial indexes can be really useful there, as well as order by (id+0). But it's a total hack.
In all my queries, the correct index is in the where clause, not the order by clause: When I removed the use of the wrong index, (ORDER BY update_at + range '0 day' forces the use of the correct index), the query used less resources and was much faster.
Of course, there may be cases where the index in the order by clause is the best one to use, but in my use this never happened.
> When I removed the use of the wrong index, (ORDER BY update_at + range '0 day' forces the use of the correct index), the query used less resources and was much faster.
I don't know about the "range" keyword. When I try that in PostgreSQL, I get a syntax error, though the "interval" keyword works. However, that produces a much worse plan:
As expected, because of the high-cardinality (~1e7) for any given organization_id, because the pages will have to be accessed randomly, and because they all have to be sorted before the limit can be applied, there's no benefit to the using foo_organization_id_idx. Sure enough, it has to do a table scan, then a sort, before it can apply the limit, and it's about 6000x slower.Finally, just for good measure, if I also drop the foo_updated_at_idx index, this is the query plan
Running the query in this case takes 4.2s. It's back to using a TEMP B-TREE index for ORDER BY clause, but it has to scan the table.For me, it's hard to draw any hard-and-fast lessons from this. In my experience, database performance is highly context dependent. It depends on the volume and distribution of data, the hardware, its current load, the indexes in place, and how the query is written. These little experiments to me just serve to confirm that.
I guess the one hard-and-fast lesson I personally take from this is not to be so certain that there is "a correct query plan" and that I or really anyone knows better than the database what that is. Sometimes in certain circumstances that may be true, but usually it's not.
> is not to be so certain that there is "a correct query plan"
Yes, data distribution, type, row size, machine power, SSD vs HD can affect which is the “best plan”.
Also, if each page contains data from all organizations then sounds very much like it's not clustered by organization_id. Can you clarify?
The default value of the column:
> Also, if each page contains data from all organizations then sounds very much like it's not clustered by organization_idThe sample data is clustered by foo_created_at_idx, I changed it to be clustered by foo_organization_id_idx
I simplified the SQL a little bit - I don't think it's necessary to have two timestamps and cluster on one of them. The other timestamp is not correlated, so the access through that index will be random anyway. And I used 10M rows only.
For me, this produces: So perhaps there's something "wrong" with your data, because there's no "Filter" or "Rows Removed by Filter" in your query plans.Of course, adding a composite or partial index may help, but people are confused why the optimizer is not smart enough to just switch to the other index, which on my machine does this:
That's ~2 orders of magnitude faster, but the optimizer has no way to predict this.Sure, the composite/partial indexes will do much better, but my intent was to explain why LIMIT queries have this problem.
Was it? The whole discussion started when zac23or said at the top of the thread that their biggest problem with PostgreSQL is its optimizer, that this is their worst example, that it needs an index on (organization_id, created_id) for it to be solved, and that SQLite and MS SQL Server do not need that index on (organization_id, created_at). They didn't initially say how their data are distributed other than that there are millions of rows with organization_id=10 and they didn't initially say that their data are skewed. Later, they said that the data are "distributed equally" and added that organization_id is random, which would be consistent, though they did say that their data are clustered not by created_at but by organization_id. Consequently, the skewed distribution seems to be a special case that you added in this sub-thread. That's fine, but that's a different albeit related problem from the one zac23or posed. If the original problem is, "Arrange for PostgreSQL to perform well on this query on this data model for equally-distributed data without adding an index on (organization_id, created_id)." that problem is solved. It can be done. If your additional problem is "Arrange for PostgreSQL to perform well on this query on this data model for unequally-distributed data without adding an index on (organization_id, created_at)." that problem is also solved with a partial index on just (created_at).
> Of course, adding a composite or partial index may help, but people are confused why the optimizer is not smart enough to just switch to the other index, which on my machine does this:
I'm curious how you forced the optimizer to switch to that query plan if it's not smart enough to do it on its own. I don't know, but I suspect you ordered by an expression, with something like
That produces nearly identical results on my machine, which increases my suspicion. This plan is better for organization_id=10 than the one the optimizer chose, without resorting to a partial index, but it comes at a cost: this plan is worse for any of the other values of organization_id, whose data are not skewed.I think a third question, which is implicit in your comments is, "Well then why doesn't the optimizer use one plan for organization_id=10 and the other plan for the other values of organization_id?" That's a good question. Is this possible? Does any other database do this? I don't know. I'll...
I don't know, but that's how I understood the discussion - I tried to explain why the Postgres optimizer makes this choice, and why planning this type of queries (LIMIT + WHERE) is a hard problem in principle.
>> Of course, adding a composite or partial index may help, but people are confused why the optimizer is not smart enough to just switch to the other index, which on my machine does this: > > I'm curious how you forced the optimizer to switch to that query plan if it's not smart enough to do it on its own.
I simply dropped the index on created_at, forcing the planner to use the other index.
> I think a third question, which is implicit in your comments is, "Well then why doesn't the optimizer use one plan for organization_id=10 and the other plan for the other values of organization_id?" That's a good question. Is this possible? Does any other database do this? I don't know. I'll try to find out but if somebody already has the answer, I'd love to hear it.
The answer is that this does not happen because the optimizer does not have any concept of dependence between the two columns, or (perhaps more importantly) between values in these two columns. So it can't say that for some departments it's not correlated but for ID 10 it is (and should use a different plan).
Maybe it would be possible to implement some new multi-column statistics. I don't think any of the existing optional extended statistics could provide this, but maybe some form of multi-dimensional histogram could? Or something like that? But ultimately, this can never be a complete fix - there'll always be some type of cross-column dependency the statistics can't detect.
> A fourth question is, "How do other databases perform on this and similar queries?"
I'm not sure what all the other databases do, but I think handling this better will require some measure of "risk" or "plan robustness". In other words, some ability to say - how sensitive is the plan to estimates not being perfectly accurate, or maybe some other assumptions being violated. But that's gonna be "better worst case, worse average" solution.
That may be a hard problem. If it is, then a harder problem still is limit + order by. That's why I almost always tell people that limit is not what you want and probably never should be used outside of ad-hoc data exploration.
The planner wasn’t particularly sophisticated in those days, nor was the data volume crushing.
But in the end, we would take all of our queries, and check their plans. We would pad the db with data until the combination of data and statistics and the planner all did what we wanted.
Once we were happy with the statistics and the results we’d reset the database with “empty” data for the customer, and never touch the statistics again.
We were mostly trying to eliminate table scans on simple queries and joins, and influencing index selection.