Hierarchical Structures in PostgreSQL (2020)
hoverbear.org
hoverbear.org
It’s one of the best books for data model design. Highly recommend.
I like paths for representing hierarchy, but closure tables can also be a good idea, depending on what you're modelling and how you query it.
That's the general case, but more specific CTE queries can be optimized, e.g. by adding database indexes. Recent versions of Postgres have greatly improved wrt. not making CTE's overly inefficient.
But I still think it's useful to know the flattening idea. I've seen it show up other places like map reduce. It's a useful general idea imo.
The only downside is that this requires updates of a large number of rows whenever the tree changes (not all rows though). If you have a sharded table that could be problematic.
Reference: [1] http://patshaughnessy.net/2017/12/14/manipulating-trees-usin... [2] http://patshaughnessy.net/2017/12/15/looking-inside-postgres...
I always just use a parent_id and recursive cte - it's plenty fast even without materialised views.
Or rather, recursive CTEs inherently require that you select only what you need.
A little difference that the non-recursive case of "I'll just use a CTE for same functionality but better readability" which can deceptively bite you prior to PostgreSQL 12.
I like to use Dunbar's Number (100-250) to approximate the levels of heirarchy in human organizations. The idea is that these organizations are most efficient when organizational layers don't exceed ~150 elements, due to the implementation details of the human brain.
Basically, you can do log_{150}(N) to get a very rough idea of how complex the organization of N people should be. This works for small startups and entire countries. Of course, startups should probably get comfortable with the idea of teams well before hitting >100 employees. Teams can then scale into departments (with new subteams), and once there are many departments, add regional layer, strategic/executive layer, and so on.
One interesting fact is that the USA population has roughly increased by a multiple of Dunbar's number since its organizational structure was codified in its Constitution. Perhaps time for another look?
Remember, it represents the total number of stable social relationships a person can maintain. If you're looking to allow your employees to have personal lives, you'll want to leave ample room for their family and friends.
Maybe an important question to ask is, how much of your employees' social-emotional carrying capacity is it appropriate to consume? If 10%, then 15-25 is your number. If 20%, then 30-50 is your number.
It's also definitely a upper limit rather than lower limit. Big bureaucracies with many layers of management and small teams can work well, but no one can really individually manage 1000 subordinates.
I have had that exact same idle thought.
In 1813, each of the 182 US Representatives represented on average ~40,000 Americans. Today, each of our 435 Reps stands for about ~760,000 people. That's over an order of magnitude growth. To keep the same rate of representation, we'd need to have over 8,000 Representatives, which is clearly too large a body to get anything done.
So we're probably well beyond the point where we could benefit from a large House of Subrepresentatives and then a smaller House of Superrepresentatives that aggregate them.
In practice if a menu has more than 3 levels the user experience will suffer. I keep them at 2 submenus max, possibly 1 if I can get away with it.
Not to mention most menus are not dynamic, meaning they can be just a JSON file or simple HTML.
I've implemented this in the past using the classic adjacency list, materialized path or nested sets mechanisms - all by using Django Treebeard, which offers a very robust implementation of all three of them: https://django-treebeard.readthedocs.io/en/latest/
It's pretty funny that I landed on this implementation, given that I spent a couple years building https://github.com/ClosureTree/closure_tree (one of the most popular acts-as-hierarchy ActiveRecord gems), as it (unsurprisingly) uses closure trees.
When CTE isn't available, closure trees are nice, but boy howdy, does that closure tree table get gigantic with deeper graphs.
If CTE is available, closure trees don't even come close in performance and simplicity.
(Hint: materialized paths should use a unique separator: ASCII 0x1F is applicable: https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Field_...)
It was hard to decide on the ltree "path" format. There's a "path" value for each comment and it is what sets up the hierarchy. What I ended up using was this (root level comments):
1.aaa
1.aab
1.aac
1.aad
.
.
.
1.aaz
1.aba
1.abb
The "1" is the post ID. Sub-comments under the first comment above would be:
1.aaa.aaa
1.aaa.aab
1.aaa.aac
The reason why I used this "path" format is because if I sort alphabetically then the comments will be in the correct "vertical" order to display chronologically.
[1] https://www.peachesnstink.com
[2] https://github.com/ferg1e/peaches-n-stink/blob/master/db/ind...
[3] https://github.com/ferg1e/peaches-n-stink/blob/master/db/ind...
But you could always traverse graphs with recursive SQL — it’s just less pleasant, and perhaps less performant — but often that’s all you need (and often people confuse having a graph relationship with needing a graph-specialized database)
My use case a few years ago was such that _every_ dedicated graph engine I tried would just OOM on the test set of ~10M nodes, which I attributed to insufficient pruning. Postgres was able to handle it, and still worked on the production set of ~1.5B nodes. The SQL took some tuning, and wasn't _pretty_, but it did _work_ where nothing else did...
create table post_paths (
descendant_id bigint not null references posts (id) on delete cascade on update cascade,
ancestor_id bigint not null references posts (id) on delete cascade on update cascade,
depth int not null,
primary key (descendant_id, ancestor_id)
);
At the cost of O(n^2) rows per hierarchy, it will allow you to do some pretty nifty things, most notably being able to get a count of all descendants of a parent in O(n) time. Updating entries may be painful, though.It'd be helpful to see queries at the domain level to understand how they fit into a problem; rather than this weird embedding of some implicit idea.
Dunno how close it is to being ready for production, but it lets you use Cypher in PostgreSQL, which is a pretty damn nice language for doing many of the things you'd use recursive CTEs for. I don't much care for Neo4j (the "home" database for the Cypher language) but Cypher is good.
https://stackoverflow.com/questions/54907495/postgresql-recu...
Edit: Sorry for being unclear. By "this" I meant the ltree solution that's listed second in the blog post.