A recent discussion in #django suggested “what we need is a PG slugify function”.
The actual algorithm in Django for this is fairly simple, and easy to follow. Shouldn’t be too hard to write it in SQL.
Function slugify(value, allow_unicode=False).
Convert to ASCII if allow_unicode is false
Remove characters that aren’t alphanum, underscores, hyphens
Strip leading/trailing whitespace
Convert to lowercase
Convert spaces to hyphens
Remove repeated hyphens
(As an aside, the comment in the django function is slightly misleading: if you followed the algorithm there, you’d get a different result with respect to leading trailing whitespace. I shall submit a PR).
We can write an SQL function that uses the Postgres unaccent extension to get pretty close:
However, it’s worth noting that the way Django connects to a database uses a single database user. Indeed, if your users table is in your database, then you’ll need some way to connect to it to authenticate. I haven’t come up with a nice way to use the Postgres users for authentication within Django just yet.
I did have an idea about a workflow that may just work.
Single Postgres User is used for authentication (Login User).
Every Django user gets an associated Postgres User (Session User), that may not log in.
This Session User is automatically created using a Postgres trigger, whenever the Django users table is updated.
After authentication, a SET SESSION ROLE (or SET SESSION AUTHORIZATION) statement is used to change to the correct Session User for the remainder of the session.
Then, we can implement the Postgres Row Level Security policies as required.
Initially, I had thought that perhaps the Session Users would have the same level of access as the Login User, however tonight it occurred to me that perhaps this could replace the whole Django permissions concept.
We do have a few things we need to work out before this is a done deal.
The trigger function that performs the CREATE ROLE statement when the django users table is updated.
Some mechanism of handling GRANT and REVOKE statements.
Similarly, some mechanism for showing current permissions for the given user.
A middleware that sets the SESSION USER according to the django user.
The simplest part of this is the last one, so we will start there. We can (in the meantime) manually create the users and their permissions to see how well it all goes. No point doing work that doesn’t work.
You’ll see we are using roles of the form django:<id>, which need to be quoted. We use the user id rather than the username, because usernames may be changed.
We’ll want to create a user for each of the existing Django users: I currently have a single user in this database, with id 1. I also have an existing SUPERUSER with the name django. We need to use a superuser if we are using SET SESSION AUTHORIZATION, which seems to be the best. I haven’t found anything which really does a good job of explaining how this and SET SESSION ROLE differ.
We should be able to do some magic there to prevent a user toggling their own superuser status.
Let’s investigate writing a trigger function that creates a new ROLE when we update the django user.
CREATEORREPLACEFUNCTIONcreate_shadow_role()RETURNSTRIGGERAS$$BEGINEXECUTE'CREATE USER "django:'||NEW.id||'" NOLOGIN';EXECUTE'GRANT "django:'||NEW.id||'" TO django';RETURNNULL;END;$$LANGUAGEplpgsqlSECURITYDEFINERSETsearch_path=public,pg_tempVOLATILE;CREATETRIGGERcreate_shadow_roleAFTERINSERTONauth_userFOREACHROWEXECUTEPROCEDUREcreate_shadow_role();
Note we still can’t create users from the admin (due to the RLS restrictions thata are there), so we need to resort to ./manage.py createsuperuser again.
Having done that, we should see that our new user gets a ROLE:
# \du
List of roles
Role name │ Attributes │ Member of
───────────┼────────────────────────────────────────────────────────────┼─────────────────────
django │ Superuser │ {django:1,django:6}
django:1 │ Cannot login │ {}
django:6 │ Cannot login │ {}
matt │ Superuser, Create role, Create DB │ {}
postgres │ Superuser, Create role, Create DB, Replication, Bypass RLS │ {}
We should be able to write similar triggers for update. We can, for example, shadow the Django is_superuser attribute to the Postgres SUPERUSER attribute. I’m not sure if that’s a super idea or not.
But we can write a simple function that allows us to see if the current django user is a superuser:
That gives us a fair bit of functionality. We still don’t have any mechanism for viewing or setting permissions. Because of the way Django’s permissions work, we can’t quite use the same trick but on the auth_user_user_permissions table, because we’d need to also look at the auth_user_groups table and auth_group_permissions.
I’m still not sure if this is a good idea or not, but it is a fun thought process.
It occurred to me tonight (after rewriting my Tax File Number generation script in JavaScript, so it works on Text Expander Touch), that it might be nice to do TFN validation in Postgres.
You can write a function that validates a TFN (this is not the first version I wrote, this one is a bit more capable in terms of the range of values it accepts):
However, we might want to convert our data into a canonical “format”: in this case, always store it as XXX-XXX-XXX. We can write a function that does this:
But how can we make our data that we insert always stored in this format?
Postgres triggers to the rescue. We can’t do it as part of a Domain (although we probably could do it as part of a scalar type, but that’s a whole other kettle of fish).
Let’s set up a table to store our tax declarations in:
Sometimes, your ORM just seems to be out to get you.
For instance, I’ve been investigating a technique for the most important data structure in a system to be essentially immuatable.
That is, instead of updating an existing instance of the object, we always create a new instance.
This requires a handful of things to be useful (and useful for querying).
We probably want to have a self-relation so we can see which object supersedes another. A series of objects that supersede one another is called a lifecycle.
We want to have a timestamp on each object, so we can view a snapshot at a given time: that is, which phase of the lifecycle was active at that point.
We should have a column that unique per-lifecycle: this makes for querying all objects of a lifecycle much simpler (although we can use a recursive query for that).
There must be a facility to prevent multiple heads on a lifecycle: that is, at most one phase of a lifecycle may be non-superseded.
The lifecycle phases needn’t be in the same order, or really have any differentiating features (like status). In practice they may, but for the purposes of this, they are just “what it was like at that time”.
I’m not sure these ideas will ever get into a released product, but the work behind them was fun (and all my private work).
The basic model structure might look something like:
classPhase(models.Model):phase_id=models.AutoField(primary_key=True)lifecycle_id=models.AutoField(primary_key=False,editable=False)superseded_by=models.OneToOneField('self',related_name='supersedes',null=True,blank=True,editable=False)timestamp=models.DateTimeField(auto_now_add=True)# Any other fields you might want...objects=PhaseQuerySet.as_manager()
So, that looks nice and simple.
Our second AutoField will have a sequence generated for it, and the database will give us a unique value from a sequence when we try to create a row in the database without providing this column in the query.
However, there is one problem: Django will not let us have a second AutoField in a model. And, even if it did, there would still be some problems. For instance, every time we attempt to create a new instance, every AutoField is not sent to the database. Which breaks our ability to keep the lifecycle_id between phases.
So, we will need a custom field. Luckily, all we really need is the SERIAL database type: that creates the sequence for us automatically.
This now ensures each time we save our object, a new instance is created. The lifecycle_id will stay the same.
Still not totally done though. We currently aren’t handling a newly created lifecycle (which should be handled by the associated postgres sequence), nor are we marking the previous instance as superseded.
It’s possible, using some black magic, to get the default value for a database column, and, in this case, execute a query with that default to get the next value. However, that’s pretty horrid: not to mention it also runs an extra two queries.
Similarly, we want to get the phase_id of the newly created instance, and set that as the superseded_by of the old instance. This would require yet another query, after the INSERT, but also has the sinister side-effect of making us unable to apply the not-superseded-by-per-lifecycle requirement.
As an aside, we can investigate storing the self-relation on the other end - this would enable us to just do:
However, this turns out to be less useful when querying: we are much more likely to be interested in phases that are not superseded, as they are the “current” phase of each lifecycle. Although we could query, it would be running sub-queries for each row.
Our two issues: setting the lifecycle, and storing the superseding data, can be done with one Postgres BEFORE UPDATE trigger function:
So, we have most of the db-level code written. How do we use our model? We can write some nice queryset methods to make getting the various bits easier:
(That was why I used the lifecycle_id as the column).
Whilst building this prototype, I came across a couple of things that were also interesting. The first was a mechanism to get the default value for a column:
defdatabase_default(table,column):cursor=connection.cursor()QUERY="""SELECT d.adsrc AS default_value
FROM pg_catalog.pg_attribute a
LEFT JOIN pg_catalog.pg_attrdef d ON (a.attrelid, a.attnum)
= (d.adrelid, d.adnum)
WHERE NOT a.attisdropped -- no dropped (dead) columns
AND a.attnum > 0 -- no system columns
AND a.attrelid = %s::regclass
AND a.attname = %s"""cursor.execute(QUERY,[table,column])cursor.execute('SELECT {}'.format(*cursor.fetchone()))returncursor.fetchone()[0]
You can probably see why I didn’t want to use this. Other than the aforementioned two extra queries, it’s executing a query with data that comes back from the database. It may be possible to inject a default value into a table that causes it to do Very Bad Things™. We could sanitise it, perhaps ensure it matches a regular expression:
However, the trigger-based approach is nicer in every way.
The other thing I discovered, and this one is really nice, is a way to create an exclusion constraint that only applies if a column is NULL. For instance, ensure that no two classes for a given student overlap, but only if they are not superseded (or deleted).
There’s a nice article at Bowled Over by SQL Window Functions by a chap called Dwain Camps. It’s written from the perspective of T-SQL, which has some differences to Postgres’s DDL and querying. I’ve reworked his stuff into what feels nice for me from a Postgressy perspective.
I’ll recap a couple of things he mentions, but you’ll probably want to head there and read that first.
Strike: all pins knocked down on the first ball of a frame. Scores 10 (from this frame), plus the total of whatever the next two balls knock down.
Spare: all pins knocked down on the second ball of a frame. Scores 10 (from this frame), plus how ever many pins you knock down on the next ball.
Open: at least one pin remains standing at the end of the frame. Score is how ever many pins you knocked down.
By convention, only the running tally is shown on the scoresheet. I’ve kept the frame score as the score for this frame only, and the total will contain the running total.
The first thing I do a bit differently is to use Postgres’ DOMAIN structures, which enables us to remove some of the check constraints, and simplify some others:
Not much different to the original, other than using those fresh new domain types.
The other approach I’ve used is to use more constraints, but make them simpler. I’m also relying on the fact that X + NULL => NULL, which means we can leave off a heap of the constraint clauses.
We’ll start by preventing the (non-final) frames from exceeding the number of available pins. In the case of final frame, we still only allow a spare unless we have a strike already.
This is as simple as it can be. Because ball 2 may be null but ball 1 may not, and each ball must be no greater than 10, this is enough to encapsulate the requirement. There is one slightly incorrect circumstance: a value of (10, 0) would be valid, which is strictly incorrect (ball 2 was never bowled). In the case of the calculations it’s all correct, but if a 0 was bowled immediately after a strike, it may be possible to insert that as ball 2, which would be misleading.
A follow-up to the previous constraint: we only get to bowl ball 3 if we have bowled ball 2, and scored a strike or spare already. Note, we may have a strike on the first ball, which means we might have more than 10.
Finally, we have some specific allowable conditions for that last ball. We already know that we must have scored a strike or spare with the first two balls, but we need to know how many pins are available to us.
If we scored a spare or two strikes, then we may have any number up to 10 to score now.
Otherwise, we have however many pins were left by ball 2 (which means ball 1 must have been a strike).
I find those constraints much easier to read that the original ones.
I’ve written a view, that uses a couple of Common Table Expressions (CTEs), as well as the window functions Dwain discussed.
CREATEORREPLACEVIEWbowling.frame_scoreAS(WITHpin_countsAS(SELECTgame_id,player_id,frame,ball1,ball2,ball3,-- Get the first ball from the next frame.
-- Used by strike and spare.
LEAD(ball1,1)OVER(PARTITIONBYgame_id,player_idORDERBYframe)ASnext_ball_1,-- Get the second ball from the next frame.
-- Used by strike.
LEAD(ball2,1)OVER(PARTITIONBYgame_id,player_idORDERBYframe)ASnext_ball_2,-- Get the first ball from the next next frame.
-- Used by double-strike.
LEAD(ball1,2)OVER(PARTITIONBYgame_id,player_idORDERBYframe)ASnext_next_ball_1FROMbowling.frame),frame_countsAS(SELECTgame_id,player_id,frame,ball1,ball2,ball3,CASE-- We will start with frame 10: when we have a strike
-- or spare, we get all three balls.
WHENframe=10ANDball1+ball2>=10THENball1+ball2+ball3-- On a strike, we get the next two balls. This could
-- be from the next frame, or include the first ball
-- of the frame after that. Note that in frame 9, we will
-- also look at the second ball from fram 10, rather than
-- looking for a non-existent frame 11.
WHENball1=10THENball1+next_ball_1+(CASEWHENnext_ball_1=10ANDframe<9THENnext_next_ball_1ELSEnext_ball_2END)-- In the case of a spare, grab the next ball. We already
-- handled a spare on frame 10 above.
WHENball1+ball2=10THENball1+ball2+next_ball_1-- Otherwise, it's just the two balls we bowled in this frame.
ELSEball1+ball2ENDASscoreFROMpin_counts)-- We have everything we need in the previous CTE, except that
-- shows us the frame score, rather than the running tally.
-- We need to do that in another window function here, but
-- only calculate a value when the frame's wave function has
-- collapsed (ie, it's score is known).
SELECTframe_counts.*,CASEWHENscoreISNOTNULLTHENSUM(score)OVER(PARTITIONBYgame_id,player_idORDERBYframeROWSUNBOUNDEDPRECEDING)ELSENULLENDAStotalFROMframe_counts);
Again, I think this is a simpler query, and easier to read. But, I guess I wrote it.
We can insert the same data as used there, and look at our results:
-- Game 1
INSERTINTObowling.frameVALUES(1,1,1,7,2,NULL),(1,1,2,3,7,NULL),(1,1,3,6,4,NULL),(1,1,4,10,NULL,NULL),(1,1,5,10,NULL,NULL),(1,1,6,10,NULL,NULL),(1,1,7,9,1,NULL),(1,1,8,10,NULL,NULL),(1,1,9,8,1,NULL),(1,1,10,6,3,NULL);-- Game 2
INSERTINTObowling.frameVALUES(2,1,1,10,NULL,NULL),(2,1,2,3,7,NULL),(2,1,3,10,NULL,NULL),(2,1,4,6,4,NULL),(2,1,5,10,NULL,NULL),(2,1,6,9,1,NULL),(2,1,7,10,NULL,NULL),(2,1,8,8,2,NULL),(2,1,9,10,NULL,NULL),(2,1,10,7,3,10);-- Game 3
INSERTINTObowling.frameVALUES(3,1,1,10,NULL,NULL),(3,1,2,10,NULL,NULL),(3,1,3,10,NULL,NULL),(3,1,4,10,NULL,NULL),(3,1,5,10,NULL,NULL),(3,1,6,10,NULL,NULL),(3,1,7,10,NULL,NULL),(3,1,8,10,NULL,NULL),(3,1,9,10,NULL,NULL),(3,1,10,10,10,10);
player_id | average
-----------+----------------------
1 | 227.0000000000000000
(1 row)
I’m fairly sure I’ve rewritten the constraints correctly, but may have missed some. Here are some of the condition tests that show invalid constraints:
This is the second post in an ongoing series dealing with storing Hierarchical or Tree data structures in Postgres. You should read the Introduction if you haven’t already.
This post contains the queries that illustrate how an Adjacency List model can be used to represent a Hierarchical set of data, including data definitions, and the various operations that have been defined in the aforementioned introduction.
They can be modelled with most ORMs without any extra infrastructure
Many of the operations are non-complex
Recursive queries allow us to perform the complex queries in reasonable time
To help build suspense (but more because I haven’t yet come up with a way to generate a nice reproducible, yet complex tree), this post may discuss the complexity of the queries, but will not contain any measurements.
Initial tree
Before each operation, our data will look like this (where parents point to children):
We will assume reversion back to this structure after each operation: we could do this using a TRUNCATE followed by an INSERT; or we could run the operation in a transaction and rollback.
There may be a post which shows the effects of each of the queries below in a graphical form.
Table structure
Adjacency Lists are dead simple. Each node simply contains a reference to it’s parent node.
Inserting a single leaf node is simple. To insert a single node as a child of node 13, for example:
INSERTINTOnodes(parent_id)VALUES(13);
Inserting a new root node is slightly more complicated. In most ORMs, we would probably do it in two queries: one to create the new node, and a second to update the other root nodes to point to this one. We’ll see that below.
We can do slightly better in raw SQL: UPDATE our table with the result of an INSERT ... RETURNING that occurs inside a Common Table Expression (CTE, or WITH query).
And our last insert, adding a child node, that gets it’s siblings as children. For instance, adding a new node under node 12, which gets all of node 12’s children as it’s children.
All of these queries should perform relatively well: the CTE will be very simple (as it is no different to the single leaf insert), and the UPDATE should likewise be fairly simple: it needs to filter out which existing nodes do not need to be updated; and then it needs to update the remainder of the rows with the value pulled from the CTE.
This theoretically is only marginally more complex than just a simple UPDATE foo SET bar=baz WHERE quux IS NULL style query.
If we were using an ORM, we might need to do this in two queries: something like this (in Django):
# Insert new root node: all other root nodes now have this as a parentnew_node=Node.objects.create()Node.objects.filter(parent=None).exclude(pk=new_node.pk).update(parent=new_node)# Could possibly do as:Node.objects.filter(parent=None).update(parent=Node.objects.create().pk)# Insert new node, with a single child as it's child (and that child's previous# parent as it's parent)new_node=Node.objects.create(parent=old_node.parent)old_node.parent=new_nodeold_node.save()# Insert new node, with children of that node's parent now children of the node.new_node=parent_node.children.create()parent_node.children.exclude(pk=new_node.pk).update(parent=new_node)# Again, may be able to do:parent_node.children.update(parent=parent_node.children.create().pk)
Note the required exclusion of the newly created node: we don’t have to do this in the CTE versions, as that doesn’t “exist” at the time the other part of the query runs.
Removals
Removing a single leaf node is no different than removing a row from a normal table. Removing node 9, for instance:
DELETEFROMnodesWHEREnode_id=9;
Because the information about parent-child relationships is stored in the child, we do not need to do anything else to maintain the tree.
To remove a single root node (in this case, node 1), and promote all children to root nodes themselves, we can do two queries:
The same pattern can be used for removing a node, and attaching it’s children to it’s parent. Here, we will remove node 2, and attach it’s children (4 and 5) as children of it’s parent, node 1:
Righto, now we are up to the traditionally “hard” things for an Adjacency List to perform. Dealing with removing an arbitrary depth of (sub)tree.
We’ll need to create a recursive CTE, and delete according to that. Let’s just select from that initially, so we can see what the CTE data will look like:
Coolio. So, on to our operations. Let’s remove the subtree starting with node 2. I’ll hide the CTE, since it will be the same for quite a few of these operations:
Because the relationship is stored purely on the child element, moving around trees and subtrees is very easy. We can start with moving subtree starting with 3 to underneath node 4:
UPDATEnodesSETparent_id=4WHEREnode_id=3;
Nothing surprising there. Similarly, the query is identical for moving a leaf to a different parent, a root node into a tree, and turning a subtree into a full tree (making that node a root node).
The final move: all of node’s children to a different node is almost as simple:
UPDATEnodesSETparent_id=5WHEREparent_id=12;
This seems to be a situation where Adjacency Lists are really good. None of these queries are any more complex than the simplest UPDATE you could think of.
Fetches
Using the same CTE, we can perform our fetches. We may need to extend it to deal with depths, but since the ancestors column contains ancestors starting with the root node, we could count stuff in there. Let’s see how it goes.
Descendants
Fetching all descendants of a given node just means we want to see if the node occurs at all in each row’s ancestors. To get all of node 10’s descendants:
Obviously, this is far less generic, but it is also significantly less complex. For starters, it only builds up the part of the tree we care about, and then just returns the node ids, rather than building up the whole tree, and then discarding the parts that are not required.
The same code can be used for determining the number of descendants, but with a COUNT(node_id) in the final query.
To get our depth-limited query, we can approach from two directions. To get the subtree to depth 2 from above:
To do the equivalent of the hand-built CTE, we would need to start with the node, and build back the other way. It’s getting late here, so I can’t think of a way to do this right now that doesn’t get stuck doing infinite recursion.
The count query is an interesting one: we can just remove the need to unnest, and take the cardinality:
The depth query is a little trickier. We want to know the ancestors of node 15, up to a depth of 2. If our ancestors array was in the reverse order, we should be able to unnest and limit.
We can do this because a node only has one parent: so limiting the number of ancestors (when sorted nearest ancestor first) is the same as limiting the depth.
Special queries
Fetching all leaf nodes is just a matter of excluding those that have a relationship to another node as it’s parent:
Well, that’s our operations. Most of them are really simple. For anything that requires us to fetch or delete a whole subtree, we needed to revert to recursive CTEs, and for some of the other operations, using a CTE makes it simpler (and easier to understand).
Next, we will look at an alternative to the CTE operations, using a recursive view. From there, we should be able to look at a trigger-based approach that materializes our tree (node, ancestors) data in a table, and keeps it up to date. That’s, as I hinted, getting close to a Materialized Path approach, but keeps the conceptual simplicity of the Adjacency List, and hopefully prevents possible issues relating to referential integrity.
I’ve written before about using Adjacency Lists, and even done some performance tests on querying them. Whilst reading a post today, it occurred to me that it might be worthwhile to do a comparison of the various methods of storing hierarchical data in Postgres, and the costs of the same operations on each of those.
This post is just an introduction, with an outline of what I plan to do to run these tests. Please feel free to suggest things that I have missed, or that might be an oversight at my end.
Tree Models
There are four methods of storing the relationships that might form a tree. This analysis will be limited to actual tree, rather than graph structures (no cycles). I plan to detail the data structures in a series of posts, one per method. In each case, where there are multiple ways to store the data, I will attempt to examine each of these.
Adjacency Lists, being the simplest to understand (and the ones I have spent more time on recently), will be discussed first.
Path Enumerations will be next, with a comparison of storing the data using the ltree extension, and using an ARRAY column.
Following this, I’ll make an attempt at using the Closure Table model: where each ancestor-descendant relationship is stored, rather than just the parent-child relationship.
Finally, I’ll have a crack at the Nested Set model. I’m not solidly behind this model for the types of data I’ve had to deal with, but it is a valid mechanism for storing and retrieving this data. Besides, it will be an interesting exercise to implement.
My plan to handle all of these is that all tree manipulation should be “automatic”, that is, adding a node (or removing one, or whatever) should not require explicit updating of the various metadata. This should all be handled by trigger functions on the tables themselves. Whether this turns out to be reasonable we shall see.
Operations
I plan to perform the same set of operations (on the same data, rather than randomly generated data) in all models, and compare the complexity and run-time of the various queries. I’m hoping to cover all of the operations that might be performed on a tree structure, so please add any more to the comments.
The data stored in the table will contain more than one tree: this means we can perform operations which add/remove root nodes/whole trees.
Insertions
Insert a single leaf node
Insert a single root node (all existing root nodes will then point to this)
Insert a single node partway through the tree, with the “replaced” node becoming a child of this (and this one keeps it’s children)
Insert a single node partway through the tree, with this node’s parent’s existing children all becoming children of the new node.
Removals
Remove a single leaf node
Remove a single root node (all children of this are promoted to root nodes)
Remove a single node partway through the tree: all children of this node then have their grand-parent as their parent.
Remove a subtree (a single non-root node and it’s descendants)
Remove a whole tree (a single root node and all descendants)
Remove all descendants of a specific node (but not the node itself)
Moves
Move a subtree from one parent to another
Move a single leaf node to a different parent
Move a root node into a tree
Make a subtree into a tree (turn a node into a root node).
Move all children of a node to a different parent
Fetches
Fetch all descendants of a given node
Fetch the number of descendants of a given node
Fetch descendants of a given node to a given depth
Fetch the number of descendants of a given node to a given depth
Fetch all ancestors of a given node
Fetch the number of ancestors of a given node
Fetch ancestors of a given node to a given depth
Fetch the number of ancestors of a given node to a given depth I don’t think this makes any sense.
Postgres’ range types are very cool. You can use them to store, as you may guess, a value that is a range. There are several included range types, and it’s possible to create your own range type. For now, we’ll just look at using a simple int4range: although everything in principle could be applied to any range type.
Firstly, a quick discussion of how the range types work.
There is a literal form, and then a functional form:
The first thing you’ll notice is that postgres converts them to canonical form. We can provide the bound-types: inclusive or exclusive upper and lower values. This is the same notation you might see if you have done some mathematics.
You can also omit the upper and/or lower bounds to get a range object that goes to ±Infinity. Finally, you can also have empty ranges.
There are several operations that can be performed on a range. The most interesting one from the perspective of this article is the UNION operation.
You’ll get an error if your union does not result in a contiguous range. There is no way to store a discontinuous range in postgres (but you could store them in an array of int4range[], for instance).
What about if we want to get the aggregate range from a set of ranges?
This actually works to some degree: but only if the ranges in the query are already sorted, as each iteration of the aggregation function must result in a valid range:
Instead, we have to either collect all of the items in an array, sort this, and then attempt to aggregate, or, at each step, aggregate as much as possible, and add the current element to the array if we cannot perform a union.
The simpler of these (but will take more memory) is to stick them all into an array, and then sort and apply the union. We only need to define one new function to do it this way:
But what about the other case? What if we care more about what data is missing?
After spending way too many hours on playing around with this, I hit on the idea of using a window function, lead to get the data from the “next” row in the query.
This gets us most of the way. We can see the ones with upper >= lead indicate there is no gap between the ranges, so we can filter. However, we need to do this with a subquery to be able to get access to the columns correctly:
It never surprises me when I find another neat feature of Postgres that makes doing a potentially difficult task simple. Today, I discovered that since 9.0, Postgres has supported a really powerful way to horizontally partition data into separate tables.
For those who haven’t heard of the concept before, horizontal partitioning is where different rows are stored in different tables, depending upon something about the data within the row.
For instance, we could partition audit data into tables based upon the timestamp of the action. Thus, all entries created during October, 2014 would be stored in a table called audit_2014_10, and entries created during March 2011 would be stored in a table called audit_2011_03. And so on. Alternatively, we could have a single table per year, or however we want to partition.
This is called “Range” partitioning.
There are a couple of ways you could think about doing this in a DBMS. You could have a writable view that redirects the writes to the correct table. The problem then is that when you add a new child table, you need to rewrite your view.
If we inherit one table from another, and do a query on the parent table, we will also get the rows that match the query from all child tables. That obviates the need for a view that uses UNION ALL or similar to fetch the data.
I’m going to use a toy example here, that just contains a single column.
This, however, is only part of the picture. Any data that is added to either of the child tables (or indeed the parent table, but we won’t be doing that), will be returned when we query the parent table.
But we want to ensure that data is partitioned out nicely. For that, we can use a trigger.
A naïve trigger may look something like:
CREATEORREPLACEFUNCTIONdata_insert_trigger()RETURNSTRIGGERAS$$BEGINIF(NEW.value>='2014-01-01'ANDNEW.value<'2015-01-01')THENINSERTINTOdata_2014VALUES(NEW.*);ELSIF(NEW.value>='2015-01-01'ANDNEW.value<'2016-01-01')THENINSERTINTOdata_2015VALUES(NEW.*);ELSERAISEEXCEPTION'Date out of range. Please fix the data_insert_trigger() function.';ENDIF;RETURNNULL;END;$$LANGUAGEplpgsql;
As you can see by the ELSE clause, we will actually need to do maintainence on this function as we start to get data that falls outside of our existing ranges. We will also need to create a new table for those rows.
It would be nice if we could automatically create tables that are missing, and handle any arbitrary values.
CREATEORREPLACEFUNCTIONdata_insert_trigger()RETURNSTRIGGERAS$$DECLAREtable_nametext;yearinteger;starttext;finishtext;BEGINyear:=date_part('year',NEW.value);table_name:='data_'||year;start:=year||'-01-01';finish:=(year+1)||'-01-01';PERFORM1FROMpg_tablesWHEREtablename=table_name;IFNOTFOUNDTHENEXECUTE'CREATE TABLE '||quote_ident(table_name)||' (CHECK ("value" >= '||quote_literal(start)||' AND "value" < '||quote_literal(finish)||')) INHERITS (data)';ENDIF;EXECUTE'INSERT INTO '||quote_ident(table_name)||' VALUES ($1.*)'USINGNEW;RETURNNULL;END;$$LANGUAGEplpgsql;CREATETRIGGERdata_insert_triggerBEFOREINSERTONdataFOREACHROWEXECUTEPROCEDUREdata_insert_trigger();
You would also want to create any indexes on the child tables, as this is where they need to be, rather than the parent table.
This function is pretty neat: it first stores what the table name should be in a variable, as well as the two bounds for this table (start and finish). Then, we see if that table exists, and if not, create it. Finally, we then insert the values into the correct child table. I’m not sure I’d recommend using it as-is: it’s quite possibly subject to a race condition if two new records came in at the same time.
The one thing that was concerning me was that DDL changes to the parent table would not propagate to the child tables: however this turned out to not be an issue at all. Since I mostly use Django, I want as little hard stuff that would require custom migration operations.
The other thing worth noting is that Postgres will do a really good job of limiting the tables that are accessed to those that contain the relevant data:
QUERY PLAN
---------------------------------------------------------------------------------------
Append (cost=0.00..42.10 rows=12 width=8)
-> Seq Scan on data (cost=0.00..0.00 rows=1 width=8)
Filter: ((value > '2014-01-01 00:00:00'::timestamp with time zone) AND
(value < '2014-07-01 00:00:00'::timestamp with time zone))
-> Seq Scan on data_2014 (cost=0.00..42.10 rows=11 width=8)
Filter: ((value > '2014-01-01 00:00:00'::timestamp with time zone) AND
(value < '2014-07-01 00:00:00'::timestamp with time zone))
Planning time: 0.292 ms
(6 rows)
We see that only the data_2014 table is hit by the query. If your constraints do something like cast to DATE, then this may not happen. This was causing me some concern earlier, but letting Postgres coerce the data types fixed it.
However, you can’t use a tstzrange to query if you want these constraints to help the query planner:
-- Actually hits every table.
SELECT*FROMdataWHEREvalue<@'[2014-01-01, 2014-07-01)'::tstzrange;
It’s worth noting that if you change a value that would cause that row to belong in a different partition, this will fail.
There are moves afoot to have this feature more tightly integrated into Postgres, perhaps using a syntax like:
This is a followup/replacement post for this older post. In it, I discussed a way to use PL/python to perform function (and in turn operations) on JSON data. Don’t do that, it’s way too slow.
It’s actually possible, using raw SQL (and some handy functions) to perform some of the operations that are “missing” from the JSON(B) datatypes in Postgres.
In all cases, I’ll work with just JSONB as the input/output formats. In practice, when I first wrote these, I wrote up to four versions of each (JSON+JSON, JSON+JSONB, JSONB+JSON, JSONB+JSONB). I believe it’s possible to write polymorphic versions of these functions, but I’m not that familiar with them just yet.
We’ll start with concatenation: basically joining two JSONB objects into one. This is a good one to start with, as operands must be either JSON or JSONB, so all forms of this function are the same, just with different functions or casting of operands.
Before I begin, I’ll mention a post I saw on Michael Paquier’s excellent blog: Manipulating jsonb data by abusing of key uniqueness. In it, Michael uses the json_object_agg function to build up a JSON object from a query:
This seems like a good idea, however it actually performs around two orders of magnitude slower than just iterating over the objects and building them up using string_agg and ||:
My attempt at reimplementing the json_object_agg aggregate function in SQL proved even slower. Not at all surprising.
The next one we will tackle is the - operator from Hstore.
hstore - text : delete key from left operand
hstore - text[] : delete keys from left operand
hstore - hstore : delete matching pairs from left operand
We can reimplement these for JSON: first as functions, and then create operators using those if we want. What is interesting is that we use the same construction pattern for our output JSONB object, however, I don’t seem to be able to figure out how to extract this out into another function.
CREATEORREPLACEFUNCTION"jsonb_subtract"("json"jsonb,"remove"TEXT)RETURNSjsonbLANGUAGEsqlIMMUTABLESTRICTAS$function$SELECTCASEWHEN"json"?"remove"THENCOALESCE((SELECT('{'||string_agg(to_json("key")::text||':'||"value",',')||'}')FROMjsonb_each("json")-- Until this function is added!
WHERE"key"<>"remove"),'{}')::jsonbELSE"json"END$function$;CREATEOPERATOR-(LEFTARG=jsonb,RIGHTARG=text,PROCEDURE=jsonb_subtract);
You’ll notice that there’s a test for if the key to remove is in the object first: this should be much faster in the situation where it doesn’t appear, as we then don’t need to recreate the object.
The other forms are quite similar, but the WHERE clause varies, and the initial test varies or is removed:
CREATEORREPLACEFUNCTION"jsonb_subtract"("json"jsonb,"keys"TEXT[])RETURNSjsonbLANGUAGEsqlIMMUTABLESTRICTAS$function$SELECTCASEWHEN"json"?|"keys"THENCOALESCE((SELECT('{'||string_agg(to_json("key")::text||':'||"value",',')||'}')FROMjsonb_each("json")WHERE"key"<>ALL("keys")),'{}')::jsonbELSE"json"END$function$;CREATEOPERATOR-(LEFTARG=jsonb,RIGHTARG=text[],PROCEDURE=jsonb_subtract);CREATEORREPLACEFUNCTION"jsonb_subtract"("json"jsonb,"remove"jsonb)RETURNSjsonbLANGUAGEsqlIMMUTABLESTRICTAS$function$SELECTCOALESCE((SELECT('{'||string_agg(to_json("key")::text||':'||"value",',')||'}')FROMjsonb_each("json")WHERENOT('{'||to_json("key")::text||':'||"value"||'}')::jsonb<@"remove"-- Note: updated using code from http://8kb.co.uk/blog/2015/01/16/wanting-for-a-hstore-style-delete-operator-in-jsonb/
),'{}')::jsonb$function$;CREATEOPERATOR-(LEFTARG=jsonb,RIGHTARG=jsonb,PROCEDURE=jsonb_subtract);
There is also the #= operator for hstore: but this seems to be record #= hstore, rather than hstore #= hstore. I’m not sure how to implement this, but I can implement a jsonb #= jsonb function:
Finally, there are operators %% (convert to array of alternating key/value pairs), and %# (convert to 2-dimensional array of keys, values).
I haven’t figured out a way to create these yet either.
So, that’s what I’ve got so far. Obviously, these will be slower than pure C implementations however, we can run some benchmarks against the hstore operators for comparisons.