Live data from Hacker News

Move Fast and Migrate Things: How We Automated Migrations in Postgres

benchling.engineering

31–40 of 55 posts

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#31
This covers a lot of ground that I've recently had to learn the hard way.

One item I've been considering; under Downtime, a reason for flakes in migrations is "long running transactions".

I've seen this too, and wonder if the correct fix is actually to forbid long-running transactions. Typically if the naive long-running transaction does something like:

    with transaction.atomic():
        for user in User.objects.all():
            user.do_expensive_thing_to_related_objects()

You can often recast that migration to something more like

    for user in User.objects.all():
        with transaction.atomic():
            user = User.objects.get(id=user.id)  # Read the row to lock it; or do a SELECT FOR UPDATE
            user.do_expensive_thing_to_related_objects()
This example is somewhat trivial, but in most cases I've seen you can fetch your objects outside of the transaction, compute your expensive thing, and then lock your row for the individual item you're working on (with a sanity-check that your calculation inputs haven't changed, e.g. check the last_modified timestamp is the same, or better that the values you're using are the same).

I've considered simply configuring the DB connection with a very short connection timeout (something like 5 seconds) to prevent anyone from writing a query that performs badly enough to interfere with other tables' locks.

Anyone tried and failed/succeeded in making this approach work?

The other subject that's woefully underdeveloped is writing tests for migrations; ideally I want to (in staging) migrate the DB forwards, run all the e2es and smoke tests with the pre-migration application code, migrate back (to test the down-migration), run the e2es again, and then really migrate forwards again. That would cover the "subtly broken deleted field" migration problem.

But how do we test that our migrations behave correctly in the face of long-running transactions? I.e. what's the failing test case for that bug?

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#32

I am convinced that data migration is definitely one of the hardest problems in data management and systems engineering. There are basically no solutions today that satisfy fundamental requirements such as minimizing downtime and guaranteeing correctness. It is _such_ a huge problem that most inexperienced developers see kicking the problem down the line with NoSQL document storage as a viable alternative (it isn't;…

I don't know, at my current job I've been introduced to DACPACs[1] and I really like it. The ability to do schema/data compares between arbitrary environments and generate the migrations in real time is awesome. [1] - https://docs.microsoft.com/en-us/sql/relational-databases/da...

Doesn't Django already do this?

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#34

I am convinced that data migration is definitely one of the hardest problems in data management and systems engineering. There are basically no solutions today that satisfy fundamental requirements such as minimizing downtime and guaranteeing correctness. It is _such_ a huge problem that most inexperienced developers see kicking the problem down the line with NoSQL document storage as a viable alternative (it isn't;…

You say NoSQL document but have you work with graph databases like neo4j that allow for flexible schemas? I think they mostly solve this problem but don't have the uptake of RDBMs because of matureness.

The problem is that without a table lock of some sort you cannot guarantee that all the documents in a store satisfy the schema definition.

The NoSQL solution is basically say, "assume that nothing could ever possibly respect any semblance of a schema and build special cases around everything". It is a very unproductive way of thinking, taking defensive programming to the extreme.

Also, joins will be a necessity for all sorts of data querying. At the end of the day, schemas are a necessity for 98% of all work, whether defined through types or ad-hoc.

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#35

How did it come to be that some portion of the industry use the term "migration" to describe changes/updates to a database? As far as I can tell, it's a really poor fit. It generates the expectation that movement of existing schema + maybe data from one host to another or one environment to another. What's usually happening instead is essentially a schema diff / mutation.

The word “replication” is used when replicating the data from one location to another. Maybe it depends on your industry but everywhere I’ve worked in software, migration has meant changing the schema and data and replication has meant moving to to another location.

I do agree that the terminology could be better, but it seems to be fairly standardised now.

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#36
Great post. I agree that we don't need automated post-deploy migrations. We just need automated pre-deploy migrations. Post-deploy migrations, for example to delete an unused column, can be implemented as pre-deploy migrations in a subsequent commit.

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#37
post #29

Earlier quoted context omitted.

One challenge with that is that the query plan often depends on the current table composition statistics. If a value is relatively common (or uncommon), or the size of the table has grown or shrunk, you may end up with a materially different (and conceivably substantially worse) plan.

I can't tell you how many times over 20 years I've heard a DBA tell me "the statistics weren't updated" after an incident.

This is my reason for preferring NOSQL where possible.

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#38

I am convinced that data migration is definitely one of the hardest problems in data management and systems engineering. There are basically no solutions today that satisfy fundamental requirements such as minimizing downtime and guaranteeing correctness. It is _such_ a huge problem that most inexperienced developers see kicking the problem down the line with NoSQL document storage as a viable alternative (it isn't;…

> NoSQL document storage as a viable alternative (it isn't; you'll be either dealing with migrating all data forever and special-casing every old version of your documents, or writing even more convoluted migration logic).

In practice (with Mongo at least) you end up with migrations being from arbitrary JSON to different arbitrary JSON, and come to rely on the Javascript runtime for anything even a bit complex. It's definitely convoluted (albeit extremely powerful), but I think the biggest issue is that generating backward migrations even if you did non-destructive operations is completely impossible.

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#39
post #29

Earlier quoted context omitted.

One challenge with that is that the query plan often depends on the current table composition statistics. If a value is relatively common (or uncommon), or the size of the table has grown or shrunk, you may end up with a materially different (and conceivably substantially worse) plan.

I can't tell you how many times over 20 years I've heard a DBA tell me "the statistics weren't updated" after an incident.

I've written cron jobs to update mysql statistics to prevent it from choosing bad query plans. It's as terrible as it sounds.

Re: Move Fast and Migrate Things: How We Automated Migrations in Postgres

#40
In our startup we moved away from Alembic to using plain SQL files for migrations, which (in our experience) is more robust and allows more control over the actual migration process. We wrote a simple migration manager class that loads a YAML config and a series of SQL files from a directory. Each migration is defined as two files of the form "[number]_[up/down]_[description].sql" and tied to a DB version, the YAML config specifies the name of a version table that contains the current version in the database. The manager then reads the current version from the table, compares it to the requested one and executes the necessary SQL files.

Alembic is great for many simple use cases but we found that for a production system it often isn't easy to maintain compatibility between two different DB systems like Postgres and SQLite anyway, as that would mean either adding a lot of feature switches and custom logic to our code or not using most of the interesting native Postgres features. Therefore Alembic offered very little benefit over a plain SQL file in terms of functionality and in addition made it harder to generate correct migrations in some case, as the auto-generation process does not work very reliably in our experience and some things are buggy/inconsistent, e.g. the creation and deletion of enum types. In addition, we found that it's much easier to write complex migration logic (e.g. create a new column and populate it with data from a complex SELECT statement) directly in SQL. Last point is that we can of course execute these migrations using any programming language / tool we like (for example we also wrote a small Go library to handle the migrations), which is a nice bonus.

That said we also heavily use SQLAlchemy in our Python backend code and like it a lot.

Post reply on HN