Common DB schema change mistakes in Postgres
postgres.ai
postgres.ai
Most of the things in this article are avoidable, and good to keep an eye out for.
But let's be clear we're not talking about the worst part of Postgres: roles. There is a ton of power there, it would be amazing to use it. Making it work feels like black magic. Every bit of the interface around it just seems like esoteric incantations that may or may not do what you expect. It's a terrible way to manage something so important.
THe manual for this section is, thin. It gives you an idea of how things should work, maybe, in a narrow use case. The problem is when they dont your going to spend time doing a lot of trial and error to figure out what you did wrong, and likely not have a clue as to how to do it right. And may god have mercy on your soul if you want to migrate a db with complex user permissions.
I need to sit down with it for a month and write "cookbook". If one person uses it and goes to bed that night without crying them selves to sleep it will have been worth it.
I suspect we aren't alone
It's just named such that when a ROLE allows `login` it's considered a user
All roles function like you would expect groups to function
A role that is not allowed to login is a `group`.
While the CREATE USER and CREATE GROUP commands still exist, they are simply aliases for CREATE ROLE.
Might as well simplify the mental model and make them the same.
> I suspect we aren't alone
Honestly I'd be happy to spend the time learning the ins and outs of PostgreSQL IAM stuff, but there's two very good reasons why I won't use it:
1. Still need the "role"/user across other services, so I don't save anything by doing ACL inside the DB.
2. I've no idea how to temporarily drop privileges for a single transaction. Connecting as the correct user on each incoming HTTP request is way too slow.
`SET ROLE`[1] changes the "the current user identifier of the current SQL session"; after running it "permissions checking for SQL commands is carried out as though the named role were the one that had logged in originally".
Whilst it changes the "current user" it doesn't change the current "session user", and this is what determines which roles you can switch to.
The docs also note that:
> SQL does not allow this command during a transaction; PostgreSQL does not make this restriction because there is no reason to.
edit: i mean yes, we need that cookbook real bad
Interesting. If you are serious about writing a cookbook for postgres roles, and open something like a kickstarter, I'll be one of the first to pledge!
I don't remember all the details, but I had to also rename the postgres user and role, which seemed a simple thing to do. But for some reason renaming the user didn't include the permissions on the database. I was left with a very confusing state of working table access and denied record access. I decided to backup the data, dropp the database and do an import that didn't include any permissions.
That simple thing turned out a complete shit show and I blame Postgres for making something so simple so complex.
What makes it complex is that there are 3 layers of objects (Database, Schema, Tables) and also implicit grants given to DB object owners
To be able to select from a table you need:
* CONNECT on the Database
* USAGE on the Schema (Given implicitly to schema owner)
* SELECT on the Table (Given implicitly to table owner)
To see these privileges we need to understand acl entries of this format
`grantee=privilege-abbreviation[]/grantor:`
* Use \l+ to see privileges of Database
* Use \dn+ to see privileges of Schemas
* Use \dp+ to see privileges of Tables
Privileges are seen [here](https://www.postgresql.org/docs/current/ddl-priv.html)
e.g. in the following example user has been given all permissions by postgres role
`user=arwdDxt/postgres`
If the “grantee” column is empty for a given object, it means the object has default owner privileges (all privileges) or it can mean privileges to PUBLIC role (every role that exists)
`=r/postgres`
Also it's confusing when Public schema is used. You have CREATE permission on schema so when the tables are created with the same user you select data with and you have owner permissions out of the box.
If roles have INHERIT, then doing the following works, no?
* Role A creates table * GRANT A TO B; * ROLE B can read from table just like A can.
Also if Role A creates new table, Role B can read that too no?
Doing ALTER DEFAULT PRIVILEGES could be another future footgun of it's own.
> * CONNECT
> * USAGE
> * SELECT
Isn't LOGIN (https://www.postgresql.org/docs/16/role-attributes.html) also needed?
Only roles that have the LOGIN attribute can be used as the initial role name for a database connectionWe managed to kludge our way to defaulting to read only, then using set role to do writes if you need to.
There you do need user with LOGIN, valid password & SSL.
TLDR, the container objects and the contained ones all share the same kind of permissions. Permissions of the container are applied to the contained unless explicitly changed.
So, if you grant select on the schema dbo to a, a will get select on all tables there. If you want to remove some table, you revoke the select on that specific table. And there is both metadata to discover where a specific privilege comes from and specific commands that edit the privileges on a specific level.
The main privileges systems includes Columns, as well as Databases/Schemas/Tables. You can SELECT from a table if you have been granted SELECT on the table, or if you have been granted it on the specific columns used in your query. ("A user may perform SELECT, INSERT, etc. on a column if they hold that privilege for either the specific column or its whole table. Granting the privilege at the table level and then revoking it for one column will not do what one might wish: the table-level grant is unaffected by a column-level operation." [1])
There's also a system of Row Security Policies [2].
[1]: https://www.postgresql.org/docs/current/sql-grant.html
[2]: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
Can confirm. Last year I implemented a simple postgREST server with rowlevel security. (The postgREST logs are really good. With cookbook and all)
The path there was somewhat difficult, but once it worked, it was truly magical. And the mechanisms involved were quite simple even.
Please do. I'd be happy to pay ~$20 for it.
Create a table to hold rows of (db,schema,table,role,read,write) configured by the admin with INSERT/UPDATE/DELETE, then a view that applies inheritance behavior and can answer whether any user can access any given resource.
E.g. you can do manual selects from internal tables to see the same content as `\dt` command for example.
If you're doing any postgres at scale, it's just a matter of time until you hit one of these conflicts. "lock_timeout" will just cause the migration to fail after the timeout, rather than just blocking all other queries.
Is there a good way to analyse a query and be informed of what sort of lock it will take?
I’ve always resorted to re-reading docs when I’m unsure.
After some experience, you start to see the reasons for locks and how they will impact you.
Or read the docs.
On the technical side, I believed waiting was due to the lock queue rather that having acquired an ACCESS EXCLUSIVE lock. The ALTER is specifically _waiting_ for any lock lower than ACCESS EXCLUSIVE to be release.
Thats why size of data is the least of your issues - its the access patterns/hotness that are the issue.
We created something like that in Citus when changing a node's hostname (e.g. during a failover). While node updates should be mutually exclusive with writes (otherwise we might lose them), we didn't want to wait for long-running or possibly frozen writers to release their locks. So after some initial waiting we'd start a background worker to kill anything that was blocking the node update.
INSERT INTO foo (bar) (SELECT max(bar) + 1 FROM foo);
can insert duplicate `bar` values when run concurrently using the default mode, since one xact might not see the new max value created by the other. You might think adding a UNIQUE index would cause the "losing" xact to get constraint errors, but instead both xacts succeed and no longer have a race condition.I can agree this is common, but then, the issue isn't with postgres, it's with software development as a whole.
* https://www.postgresql.org/docs/current/transaction-iso.html...
This is not true. What happens is that the (sub)transaction that loses the race to the index is aborted:
=# INSERT INTO foo (bar) (SELECT max(bar) + 1 FROM foo);
ERROR: duplicate key value violates unique constraint "foo_bar_idx"
DETAIL: Key (bar)=(2) already exists.I can’t say it avoids all of them but we are working on a new product that would. If you are interested in this space (and Postgres specifically), I’d love to hear from you: fabian@reshapedb.com
This is not how it works, period.
CREATE TABLE <abcv2> SELECT * FROM <abc> WHERE <>
People do it all the time, either to create a backup table, or deleting data in bulk, etc.Also make sure to set maintenance_work_mem high as it helps with index creation
If something goes pear-shaped or I really need them, I can create those indexes later.
The article doesn't provide any good example of misuse. And that's exactly how you use it. It's clean and simple, no hidden pitfalls. Schema migration tools are overhead when you have a few tables.
It did describe the misuse pretty well, though. The idea is that out-of-band schema modifications are a process/workflow issue that needs to be directly addressed. As stated by OP, this is an easy way for anomalies to creep in - what if the already present table has different columns than the one in the migration? IF EXISTS lets a migration succeed but leaves the schema in a bad state. This is an example of where you would prefer a migration to "fail fast".
In this particular case, the "bad data" is a table/column/view that exists (or doesn't) when it should(n't). Why does the table exist when it shouldn't yet exist? Did it fail to get dropped? Is the existing one the right schema? Is the same migration erroneously running twice?
After each migration, your schema should be in a precise state. If the phrase "IF [NOT] EXISTS" is in your migration, then after a previous migration, it wasn't left in a precise state. Being uncertain about the state of your schema isn't great.
> To those who still tend to use int4 in surrogate PKs, I have a question. Consider a table with 1 billion rows, with two columns – an integer and a timestamp. Will you see the difference in size between the two versions of the table
Wouldn't the important thing be an index size, not a table size? Table size already has 23-byte header + alignment padding. So 4 byte difference doesn't do much for table size. But fitting more of an index into memory could have some benefits. An index entry has 8-byte header.
Secondly, 1 billion rows (used in the example) are far too close to the maximum for int4 for comfort.
Great article nonetheless.
So I guess an 8kb page on disk could be more than 8kb in ram?
Seems to only affect working memory for table row data. Still significant (especially in Postgres where rows are randomly ordered which is horrible for locality for range queries) but not a home run insight imo.