Lesser-known Postgres features
hakibenita.com
hakibenita.com
id int SERIAL
If you are on a somewhat recent version of postgres, please do yourself a favor and use: id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
An "identity column", the part here:https://hakibenita.com/postgresql-unknown-features#prevent-s...
You might think this is trivial -- but SERIAL creates an "owned" (by a certain user) sequence behind the scenes, and so you run into massive headaches if you try to move things around.
Identity columns don't, and avoid the issue altogether.
https://www.2ndquadrant.com/en/blog/postgresql-10-identity-c...
If you start from the basic premise that the database engine and the application are intertwined and are not loosely coupled, using Postgres-specific features feels much less icky from an architectural point of view. They are essentially part of your application given that you use something like Flyway for migrations and you don't manually run SQL against your production to install functions and triggers and such.
I've used software that at least tried to be portable, so you could install it with whatever database you had available.
But, what makes this sort of design so nice is that you can use DB-specific stuff behind the interface because you’re not trying to write all your queries in the minimally supported subset of SQL or something.
> One often hears the counterargument 'but using DB-specific features makes your application less portable!'
OK, sorry for the probably stupid question: Isn't it just a matter of, for each sequence, selecting its current value and then creating the new one in the target database to start from there? Should be, if perhaps not easily, still reasonably scriptable... Or what am I missing?
As we added more features, the lack of transactions and relational structures started to slow us down, so we dropped in Postgres as a backend, and having application-generated UUID4s as primary keys was a big part in making that move fairly painless
Having said that, moving logic for id generation to the application because it's less portable otherwise is an odd reason.
A more reasonable argument for application layer ID generation is flexibility. It's likely that I want some specific primary key generation (e.g. by email, or username, or username prepended with role, and so on).
- Avoids people being able to just iterate through records, or to discover roughly how many records of a thing you have
- Allows you to generate the key before the row is saved
I think I default to auto-increment ID's due to: - Familiarity bias
- They have a temporal aspect to them (IE, I know row with ID 225 was created before row with ID 392, and approximately when they might be created)
- Easier to read (when you have less than +1,000,000 rows in a table)
I agree and think you're right in that UUID's are probably a better default.Though you can never find a "definitive" guide/rule online or in the docs unfortunately.
There are UUID variants that can work well with indices, which shrinks the case for big-integers yet further, to micro-optimizing cases that are situational.
UUIDv7 (currently a draft spec[0]) are IDs that can be sorted in the chronological order they were created
In the meantime, ulid[1] and ksuid[2] are popular time-sortable ID schemes, both previously discussed on HN[3]
[0] https://datatracker.ietf.org/doc/html/draft-peabody-dispatch...
[1] https://github.com/ulid/spec
[2] https://github.com/segmentio/ksuid
[3] ulid discussion: https://news.ycombinator.com/item?id=18768909
UUIDv7 discusison: https://news.ycombinator.com/item?id=28088213
We ended up implementing UUIDv7 in our ID generation library https://github.com/MatrixAI/js-id. And we have a number of tests ensuring that it is truly monotonic even across process restarts.
See IdSortable.
(And remember, the values are in different tables, so the only disadvantage is that your millionth user has ID 2000000. To the trained eye they look like an invoice; but there is no way for the system itself to treat that as an invoice, so it's only confusing to humans. If you use auto-increment keys that start at 1, you have the same problem. Account 1 and Invoice 1 are obviously different things.)
I agree in principle, but then you have Windows skipping version 9 because of all of the `version.hasPrefix("9")` out in the world that were trying to cleverly handle both 95 and 98 at once. If a feature of data is exploitable, there's a strong chance it will be exploited.
Re: the upper bound: if you reach a million customers, you have lots of other nice problems, like how to spend all the money they earn you :-)
You could use "is_test_account" or whatnot, but that adds an unnecessary column (for ~99% of records)
I like this -- will keep it in mind!
Positive IDs were home-grown, negative ones were Unique Feature Identifiers from some old GNS system import (from some ancient US Government/Military dataset).
Then when something had to be edited/replaced/created it would possibly change a previously negative ID to positive, fun times.
Of your disadvantages... if I wanted to know when rows were created, I'd just add a created timestamp column.
But "easier to read" is for real -- it's easy when debugging something to have notes referencing rows 12455 and 98923823 or in some cases even keep them in your head. But UUIDs are right out.
And you definitely don't want to put UUIDs in a user-facing URL -- which is actually how you get around "avoids people being able to just iterate through records", really whether you have UUIDs or numeric pks I think putting internal PK's in URLs or anything else publicly exposed is a bad idea, just keep your pks purely internal. Once you commit to that, the comparison between UUIDs vs numeric as pks changes again.
Why?
Both as a user and as a dev I love that I can just change the PK to get to a specific post, item, whatever instead of changing the whole link.
It's considered undesirable because it's basically an "implementation detail", it's good to let the internal PK change without having to change the public-facing URLs, or sometimes vice versa, when there are business reasons to do so.
And then there are the at least possible security implications (mainly "enumeration attacks") and/or "business intelligence to your competitors" implications too.
But it's true that plenty of sites seem to expose the internal PK too, it's not a complete consensus. Just kind of a principle to separate internal implementation from public interface.
Here's a short 2008 HN discussion on it, in which people have both opinions: https://news.ycombinator.com/item?id=19876901
Here's a more recent and lengthy treatment:
Wikipedia also adds: "the probability to find a duplicate within 103 trillion version-4 UUIDs is one in a billion."
Sort of the distinction between unspecified behaviour and undefined behaviour in C.
You need some UX like an error message in a red rectangle or something.
the solution might as well just be not to care about this case (no sarcasm).
Your application might not care about treating this error, but the DB will report it.
1. because PRIMARY KEY is its own constraint, and the underlying index is not under you control
2. because PRIMARY KEY further restricts UNIQUE, and as of postgres 14 "only B-tree indexes can be declared unique"
Ofc, your app performance requirements might vary, and it is objectively true that UUIDs aren't ideal for btree indexes.
Unless you are using a time sorted UUID, and you only do inserts into the table (never updates) avoid any feature that creates a BTree on those fields IMO. Given MVCC architecture of Postgres time sorted UUID's are often not enough if you do a lot of updates as these are really just inserts which again create randomness in the index. I've been in a project where to avoid a refactor (and given Postgres usage was convenient) they just decided to remove constraints and anything that creates a B-Tree index implicitly or explicitly.
It makes me wish Hash indexes could be used to create constraints. They often use less memory these days in my previous testing under new versions of Postgres, and scale a lot better despite less engineering effort in them. In other databases where updates happen in-place so as not to change order of rows (not Postgres MVCC) a BRIN like index on a time ordered UUID would be often fantastic for memory usage. ZHeap seems to have died.
Sadly this is something people should be aware of in advance. Otherwise it will probably bite you later when you have large write volumes, and therefore are most unable to enact changes to the DB when performance drastically goes down (e.g. large customer traffic). This is amplified because writes don't scale in Postgres/most SQL databases.
So if you have concurrent updates which are correlated with row insertion time, random keys can be a win. On the other hand, if your lookups are correlated with row insertion time, then the relevant key index pages are less likely to be hot in memory, and depending on how large the table is, you may have thrashing of index pages (this problem would be worse with MySQL, where rows are stored in the PK index).
(UUID doesn't need to mean random any more though as pointed out below. My commentary is specific to random keys and is actually scar tissue from using MD5 as a lookup into multi-billion row tables.)
I beleive the word you are looking for is "ought". :)
1. The UUIDs are generated in increasing order, so none of the b-tree issues others have mentioned with fully random UUIDs.
2. They're true UUIDs, so migrating between DBs is easy, ensuring uniqueness across DBs is easy, etc.
3. They also have the benefit of having a significant amount of randomness, so if you have a bug that doesn't do an appropriate access check somewhere they are more resistant to someone trying to guess the ID from a previous one.
id int SIMILAR TO SERIAL BUT WITHOUT PROBLEMS MOVING THINGS AROUND
And the other variant for when you aren’t sure that worked: id int SIMILAR TO SERIAL BUT WITH EVEN FEWER PROBLEMS MOVING THINGS AROUNDMaybe it is because I'm too old, but making id grow by sequence is the way how things 'ought' to be done in the old skool db admin ways. Sequences are great, it allows the db to to maintain two or more sets of incremental ids, comes in very handy when you keeping track of certain invoices that needs to have a certain incremental numbers. By exposing that in the CREATE statement of the table brings transparency, instead of some magical blackbox IDENTITY. However, it is totally understandable from a developer's perspective that getting to know the sequences is just unneeded headache. ;-)
Instead of using GENERATED BY DEFAULT, use GENERATED ALWAYS.An attempt to find a suffix like that will not be able to use an index, whereas creating a functional index on the reverse and looking for the reversed suffix as a prefix will be:
# create table users (id int primary key, email text);
CREATE TABLE
# create unique index on users(lower(email));
CREATE INDEX
# set enable_seqscan to false;
SET
# insert into users values (1, 'foo@gmail.com'), (2, 'bar@gmail.com'), (3, 'foo@yahoo.com');
INSERT 0 3
# explain select * from users where email ~* '@(gmail.com|yahoo.com)$';
QUERY PLAN
--------------------------------------------------------------------------
Seq Scan on users (cost=10000000000.00..10000000025.88 rows=1 width=36)
Filter: (email ~* '@(gmail.com|yahoo.com)$'::text)
# create index on users(reverse(lower(email)) collate "C"); -- collate C explicitly to enable prefix lookups
CREATE INDEX
# explain select * from users where reverse(lower(email)) ~ '^(moc.liamg|moc.oohay)';
QUERY PLAN
----------------------------------------------------------------------------------------------------
Bitmap Heap Scan on users (cost=4.21..13.71 rows=1 width=36)
Filter: (reverse(lower(email)) ~ '^(moc.liamg|moc.oohay)'::text)
-> Bitmap Index Scan on users_reverse_idx (cost=0.00..4.21 rows=6 width=0)
Index Cond: ((reverse(lower(email)) >= 'm'::text) AND (reverse(lower(email)) < 'n'::text))
(Another approach could of course be to tokenize the email, but since it's about pattern matching in particular)Blogpost discussing this approach: https://about.gitlab.com/blog/2016/03/18/fast-search-using-p...
Here is another way I have used to do pivot tables / crosstab in postgres where you have a variable number of columns in the output:
https://gist.github.com/ryanguill/101a19fb6ae6dfb26a01396c53...
You can try it out here: https://dbfiddle.uk/?rdbms=postgres_9.6&fiddle=5dbbf7eadf0ed...
For example create a table with a trigger on insert "NOTIFY new_data". Then on query do
LISTEN new_data;
SELECT ...;
Now you'll get the results and any future updates.They achieve the same thing, but [.] avoids the problem of your host language "helpfully" interpolating \. into . before sending the query to Postgres.
- generate_series(): While not the best to make _realistic_ test data for proper load testing, at least it's easy to make a lot of data. If you don't have a few million rows in your tables when you're developing, you probably don't know how things behave, because a full table/seq scan will be fast anyway - and you'll not spot the missing indexes (on e.g. reverse foreign keys, I see missing often enough)
- `EXPLAIN` and `EXPLAIN ANALYZE`. Don't save minutes of looking at your query plans during development by spending hours fixing performance problems in production. EXPLAIN all the things.
A significant percentage of production issues I've seen (and caused) are easily mitigated by those two.
By learning how to read and understand execution plans and how and why they change over time, you'll learn a lot more about databases too.
(CTEs/WITH-expressions are life changing too)
Postgres 12 introduced controllable materialization behaviour: https://paquier.xyz/postgresql-2/postgres-12-with-materializ...
By default, it'll _not_ materialise unless it's recursive, or if there are >1 other CTEs consuming it.
When not materializing, filters may push through the CTEs.
Also to note: EXPLAIN just plans the query, EXPLAIN (ANALYZE) plans and runs the query. Which can take awhile in production.
[0]: https://www.postgresql.org/docs/current/rangetypes.html
[1]: https://www.postgresql.org/docs/current/functions-range.html
I posted to pgsql-hackers about this problem here: https://www.postgresql.org/message-id/DE57F14C-DB96-4F17-925...
It's nice to see one that's full of good advice.
Would i use them in production? no. Are they fun to play around with? Yes!
It pushed alot of complexity down away from higher-level app developers not familiar with ES patterns.
[0]: https://github.com/matthewfranglen/postgres-elasticsearch-fd...
Or in words: There is an overlap if meeting A starts before meeting B ends and meeting A ends after meeting B starts.
So the scenario described looks way more complex as it actual seems to be.
SELECT * FROM meetings, new_meetings WHERE new_meetings.starts_at<meetings.ends_at AND new_meetings.ends_at>meetings.starts_at;
Those two time periods do overlap mathematically, but not physically: they share the common point of "4", even if that point has duration 0.
I know the iCal standard goes into this for a bit.
After you're done with that corner case, you may find it better to determine if they don't overlap and negate that.
meeting_a.starts_at <= meeting_b.ends_at && meeting_b.starts_at <= meeting_a.ends_at
So, indeed, the scenario as described in the article is more complex than needed.
It's not a huge deal in most cases, is a bigger issue if you are running any sort of scheduled ETL to update (or insert new) things.
https://blog.timescale.com/blog/how-we-made-distinct-queries...
SELECT *
FROM users
WHERE email ~ ANY('{@gmail\.com$|@yahoo\.com$}')
Perhaps they were intending something similar to the following example instead.
This one works but has a several potential lurking issues: with connection.cursor() as cursor:
cursor.execute('''
SELECT *
FROM users
WHERE email ~ ANY(ARRAY%(patterns)s)
''' % {
'patterns': [
'@gmail\.com$',
'@yahoo\.com$',
],
})
The dictionary-style interpolation is unnecessary, the pattern strings should be raw strings (the escape is ignored only due to being a period), and this could be a SQL injection site if any of this is ever changed. I don't recommend this form as given, but it could be improved.OP indicated as much saying:
> This approach is easier to work with from a host language such as Python
I'm with you on the injection - have to be sure your host language driver properly escapes things.
SELECT user_id, ARRAY_AGG(STRUCT(post_id, text, timestamp) ORDER BY timestamp DESC LIMIT 1)[SAFE_OFFSET(0)].*
FROM posts
GROUP BY user_id
`DISTINCT ON` feels like a hack and doesn't cleanly fit into the SQL execution framework (e.g. you can't run a window function on the result without using subqueries). This feels cleaner, but I'm not actually aware of any other DBMS that supports `ARRAY_AGG` with `LIMIT`.Does the `ARRAY_AGG` approach offer any advantages?
The `ARRAY_AGG` approach uses a simple heap, so it's as efficient as MIN/MAX because it can be computed in parallel. ROW_NUMBER however needs all the rows on one machine to number them properly. ARRAY_AGG combination is associative whereas ROW_NUMBER combination isn't.
select user_table.user_id, tmp.post_id, tmp.text, tmp.timestamp
from user_table
left outer join lateral (
select post_id, text, timestamp
from post
where post.user_id = user_table.user_id
order by timestamp
limit 1
) tmp on true
order by user_id
limit 30;
... ORDER BY date DESC LIMIT 1 BY user_id> I'm not actually aware of any other DBMS that supports `ARRAY_AGG` with `LIMIT`.
So, put ClickHouse in you list :)
I would hesitate to call this a useful feature. This error message doesn’t tell you about the actual problem at all.
How about something like:
Permission denied, cannot select other than (id, name) from table users
SELECT *
INTO TEMP copy
FROM foobar;
\copy "copy" to 'foobar.csv' with csv headers CREATE TEMP TABLE "copy" AS
SELECT * FROM foobar;Though I wish there was an easy way to figure out how many times "CONFLICT" actually occured (i.e how many times did we insert vs update).
Is that feature considered well-known? Or is it so obscure this author didn't know about it?
I assumed it would work as a regular pub/sub pattern where you get notified when some event happens. However, in the attached example, they still poll the database every half second. I'm not sure I understand the idea.
> JDBC driver cannot receive asynchronous notifications.
Maybe their example is limited by the choice of client library.
This might be a better example? https://tapoueh.org/blog/2018/07/postgresql-listen-notify/
Or the offical postgres docs are:
https://www.postgresql.org/docs/14/sql-notify.html
Quite strange that this specific driver didn't support asynchronous notifications: if you have to poll the database anyway, there's no much difference between doing it without listen/notify support, I guess.
They're the ones that will be hardest to migrate to a different database, most likely to be deprecated, and least likely to be understood by the next engineer to fill your shoes.
While many of these are neat, good engineering practice is to make the simplest thing to get the job done.
Databases often outlive applications. Database features usually do what they do very quickly and reliably. Used well, a full-featured database can make things like in-place refactors or re-writes of your application layer far easier, make everyday operation much safer, as well as making it much safe & useful to allow multiple applications access to the same database, which can be very handy for all kinds of reasons.
> While many of these are neat, good engineering practice is to make the simplest thing to get the job done.
That, or, use the right tool for the job.
Migrating to another DB will always take work, no matter how much you try to ignore flavor-specific features. Query planning, encoding and charsets, locking abilities tend to be very different. A query can run fine in MySQL and cause a deadlock in Postgres even though it's syntactically valid in both.
[0] yes, that's actually tightly coupling them, because now your DB is too unsafe to use without the "app" you built on top, and doesn't provide enough functionality to make it worth trying to retrofit that safety onto it.
I’ve since ported a few apps in early development or production between Microsoft SQL server, MySQL and Postgres (in various directions) but nothing in prod in over 10 years
db=# COMMENT ON TABLE sale IS 'Sales made in the system'; COMMENT
No affiliation with tbls except that I'm a big fan
Would these be drop in replacements for PL/pgSQL? Are there any performance tradeoffs to using one over another? Any changes in functionality (aka functions you can call)?
pl/sh also works wonderfully if you want to run a complex procedure in an isolated subprocess, but pl/sh is not part of the main postgresql distribution.
https://www.postgresql.org/docs/9.5/external-pl.html
Heck, there's even a pl/prolog out there, but it looks pretty old and I'm skeptical how useful it would be.
Enjoyed and bookmarked.
This feature might be great for OLAP systems, but it's awful for OLTP systems. The last thing I want is debug permission errors bubbling from my database into the application layer. It may not be as efficient, but you should always manage your ACL rules in the application layer if you're building an OLTP app.
I speak from personal experience. If you know a good usecase for ACL in database for OLTP workflows, I'm all ears.
Better security guarantees, for sure, but if an unauthorized user gains direct access to your database, you might have bigger problems from a security perspective.
An upside to using db to handle data permissions is the data has the same protections if you are accessing with direct SQL or using an app. Also, those permissions would persist with the data in restored backups.
I’m not advocating this and think for OLTP, building RBAC in the app layer is almost always a better idea, but these would be some benefits.
Here's one that's meaningful to me. We have a single database with five different applications that access it, each of them managed by a separate team. By enforcing access constraints in the database we guarantee the access constraints will be applied in all cases. It is difficult to ensure that in application code managed by separate teams.
(Just to be clear: I don't think your advice is poor, I just wanted to give an example of a case where it isn't universally applicable.)
Are there any risks to changing ACL rules on a production database server from a stability perspective?
i've never seen anything impact postgres stability. they test their code.
i've made DDL changes on live systems; that seems far more concerning than ACLs.