Understanding Recursive Queries in PostgreSQL
cybertec-postgresql.com
cybertec-postgresql.com
I think that people need to move beyond simple recursive sql example blog posts and share the more challenging examples. Recursive queries can become challenging quickly.
ltree is no match in power to CTEs, but it is perfect for a simple domain like the one used in the article.
[1] see also Checking a large routine, 1949
If anyone knows how to increase INSERT speeds on Postgres, I would appreciate. I can get about 100K per second with COPY or putting the inserts in a transaction. Have tried some more mirrored NVME drives and more RAM with ZFS, as well as dropping some unimportant indexes, but I'm still a little limited. Any thoughts?
Have you done things like disable constraints and indexing during loading?
Indexes don’t exist. Constraints are present in the syntax and the planner relies on them but they are never enforced. Our ETL layer deals with checking constraints at load time. Load performance is good. I’ve never run a rows/minute metric because it’s never been a problem.
Steps:
- use ext2 or even tmpfs as the filesystem, this disables journaling and COW features of ZFS; mount options async,nobarrier
- set tables to be UNLOGGED to disable Postgres WAL
This got things fast enough for my last use-case. But next ideas, as at some point you then become CPU/memory bound
- you could try sharding by partitioning the table
- another trick is to use SQLite as a backing store for inserts (it is quite fast if you turn off all logging) and then query with Postgres via a FDW https://github.com/pgspider/sqlite_fdw
The algorithm I'm working with uses single-cores but is linear-time and in my experience is at least 10x-100x faster than the IO. Memory is actually the biggest problem with the method, which is why I need to use a database to back it to disk.
If, for example, you get a 3-5x speedup without checks (not unheard of), you can do it twice, verify the copies are the same, and get a 2x speedup.
Other ideas:
- Have multiple processes inserting in parallel (this interacts with the commit_delay and commit_siblings settings in postgresql.conf)
- Use unlogged tables (but table is truncated on db startup)
- Put fsync=off and synchronous_commit=off in postgresql.conf (but db may be corrupted in a crash)
- switching to BRIN indexes might be an option, but depends on data
BRIN indexes actually work for some of the data types I'm working with! Thank you for pointing those out!
Finally, you can drop ALL indexes and recreate them after import (been hit and miss in my testing but on occasion has helped).
The basic idea is that I am using SQL to back a suffix array (https://en.wikipedia.org/wiki/Suffix_array). The suffix array has all suffixes of a string (for "bats", it would have ["bat", "at", "t"] sorted in alphabetical order ["at", "bat", "t"] except storing the index of the suffix [1, 0, 2].
To search for a string, you go halfway in the suffix array and check if the suffix is lexicographically equal to, larger than, or smaller than your input. If it is not equal to, halve in the right direction. If the string is there, eventually you'll hit it.
In my experience that's a good idea. The query planner has a really hard time with recursive queries.
You better not traverse too deep (iterate too much), or you lose the data locality benefits that relational databases are tuned for, and all you end up saving is the cost of round trips to the database.
Many tree structures are better modelled as prefix pattern matching on string paths. Extracting a whole subtree with "path LIKE 'foo/bar/baz/%'" on an indexed path column is much faster than following a series of parent links with a recursive query, more and more so the deeper your subtree gets. This denormalizes the tree structure into the path column - you can no longer make structural edits with small updates - but it's much faster for queries.
owner_id, element_path, element_id
where element_path would be something like an LTree -- to allow easily querying over the set of traversals defined in the jsonb?
Is it a reasonable way to represent the tree in convenient way for queryomg purposes while also allowing data updates to stay simple?
So you could create the materialized view which builds the path, and then use an index on the path for subtree selection. However you might find refreshing the materialized view slower than you'd like.
The only real answer here is to build it and load it out with a representative sample of data, or a set scaled down for a smaller test machine, and finding out what the latency and throughput are like for your case. Find out what the ground truth is for you, today, on today's hardware. My tests were from 4+ years ago.
I think it would be perhaps more beneficial to point out that the bigger problem with this approach that typically leads people to try other avenues including recursive queries is when storage comes at a premium or the “path” makes up the bulk of the record and there is significant duplication of leading prefixes.
The main point I'm making is that you can grab a whole subtree on a prefix match right out of a single index, whereas recursive queries need new lookups at every step. Recursive queries look cool for someone with an object-oriented mental model - hey, look, you can make the database chase pointers - but chasing pointers is still slow, whether it happens in the database or in memory.
(This was past me. I thought recursive queries looked neat for fetching tree-shaped data. So I built the data model and loaded it up, and then discovered that performance was absolute rubbish.)
One particularly memorable example he showed was a real world implementation of Dijkstra's shortest path algorithm using recursive CTEs. While it was very impressive I did spend most of the demo thinking "I'm so glad I don't have to maintain that"!
https://stackoverflow.com/questions/3187850/how-does-a-recur...