Hacker News new | ask | show | jobs
by dcosson 2685 days ago
I think DB's could definitely do more to expose what the cost of various operations are, it would be great if you could "explain" a migration before you run it like you can with a query and it would calculate a rough cost, how many rows need to be touched, what resources need to be locked, even how likely the required locks are to cause contention with other frequently-taken locks based on system statistics, etc.

But I was thinking about this recently and I feel like theres's some low hanging fruit in the migration frameworks themselves which, at least as far as I'm aware, all just completely punt on this problem. Rails, Alembic, and every other framework I've used will let you write a migration like adding a new non-nullable column, or renaming an existing column, things that can be really slow on a big or frequently written table and/or will cause problems during the rollout if an old version of the code is still running. It doesn't seem like it would be that hard to add a safe mode where the framework will block at least some of the most common variations of these unsafe migrations. Maybe it's harder than I realize, or maybe it's just a matter of anyone opening up some PR's and actually implementing this.

3 comments

> do more to expose what the cost of various operations are

Both Postgres & SQL Server support EXPLAIN on DML without running the statement (with varying levels of depth).

    CREATE TEMPORARY TABLE tmp_blah (num INT);
    EXPLAIN INSERT INTO tmp_blah SELECT * FROM generate_series(1, 100000) ORDER BY random();

    QUERY PLAN
    Insert on tmp_blah  (cost=62.33..74.83 rows=1000 width=4)
        ->  Subquery Scan on "*SELECT*"  (cost=62.33..74.83 rows=1000 width=4)
            ->  Sort  (cost=62.33..64.83 rows=1000 width=12)
                Sort Key: (random())
                ->  Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=12)
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.
How come it's not automatic and built-in? Why is a script terrible? (Does it have to enumerate all tables and piecewise run the stat update?)
Yes, the stats update is per table. We only updated it for a few tables we really cared about.
This is my reason for preferring NOSQL where possible.
I guess the issue with some of that stuff is that the solution is to do it in stages. Eg, adding a non-nullable column is made safe by doing part of the work and then updating rows in batches. Alembic runs all migrations in a transaction, so there’s not much point batching. It really needs to be handled outside alembic to make sense in the model. I guess that’s all a moot point now in Postgres 11 anyway.
What do you mean by your last sentence? How does PG 11 help?
Postgres v11 adds default values for NOT NULL columns without a full table rewrite.
> I think DB's could definitely do more to expose what the cost of various operations are, it would be great if you could "explain" a migration before you run it like you can with a query and it would calculate a rough cost, how many rows need to be touched, what resources need to be locked, even how likely the required locks are to cause contention with other frequently-taken locks based on system statistics, etc.

As part of my development process for a project I am working on currently I have spent quite a bit of time writing a Python 3 program to generate sample data.

It's nothing groundbreaking but I haven't seen anyone talk about this so I think it might be of interest to others maybe?

I'll explain how that is relevant to the comment I am replying to in a moment but first I would like to talk a little about said program.

So first of all you have other existing tools for generating sample data. For example in DBeaver (https://dbeaver.io/) you have functionality for generating mock data. However, while DBeaver as a whole is a nice tool that I am happy to have learned about, the mock data generating functionality is from what I have seen severely limited.

In particular, what I wanted to do was to generate data that would adhere to arbitrary statistical distribution of my liking.

In the first version of my program it took about 50 minutes (unacceptable!) to generate and insert 10,000,000 records on my laptop, and I was consuming so much RAM that my laptop started swapping which hurt the performance as well (to be fair my laptop only has 8GB of RAM but still). That however was just the initial starting point -- it was even only doing statistical distribution for one property (age), a few were assigned random values and all of the other were assigned a single value common to all.

After a bit of thinking I decided that the next step would be to construct a tree structure with frequencies for the different values. This tree is cheap for memory and fast to build.

First I specify a set of "distribution templates" that instruct the frequencies for values or groups of values of each of the properties that will have a desired statistical distribution.

For example, I create a demographics model (simplified here for brevity):

    distribution_templates = \
    {
      'age':
      {
        '18-24': 150,
        '25-34': 206,
        '35-44': 185,
        '45-54': 177,
        '55-64': 175,
        '65+':   106,
      },
      'gender':
      {
        'female':         80,
        'male':           15,
        'other':           3,
        'rather not say':  2,
      },
    }
From this I create at runtime what I refer to as a "combinatorial tree" with calculated target frequencies, and then I distribute a population count over it that is specified at runtime as well. Let's say that we want to generate 10,000,000 sample users. The resulting tree looks like this:

    -- root -- 'all' (10000000) -- age -- '18-24' (1501501) -- gender -- 'female' (1201201)
                                       |                              |- 'male' (225225)
                                       |                              |- 'other' (45045)
                                       |                              `- 'rather not say' (30030)
                                       |- '25-34' (2062062) -- gender -- 'female' (1649650)
                                       |                              |- 'male' (309309)
                                       |                              |- 'other' (61862)
                                       |                              `- 'rather not say' (41241)
                                       |- '35-44' (1851852) -- gender -- 'female' (1481482)
                                       |                              |- 'male' (277778)
                                       |                              |- 'other' (55555)
                                       |                              `- 'rather not say' (37037)
                                       |- '45-54' (1771772) -- gender -- 'female' (1417418)
                                       |                              |- 'male' (265766)
                                       |                              |- 'other' (53153)
                                       |                              `- 'rather not say' (35435)
                                       |- '55-64' (1751752) -- gender -- 'female' (1401402)
                                       |                              |- 'male' (262763)
                                       |                              |- 'other' (52552)
                                       |                              `- 'rather not say' (35035)
                                       `- '65+' (1061061) ---- gender -- 'female' (848849)
                                                                      |- 'male' (159159)
                                                                      |- 'other' (31832)
                                                                      `- 'rather not say' (21221)
In addition to that I have created functions for things like algorithmically generating unique usernames.

Then I have a tree walker that yields all of the combinations represented by the tree. From the tree above we would yield { 'age': '18-24', 'gender': 'female' } a total of 1,201,201 times, { 'age': '18-24', 'gender': 'male' } a total of 225,225 times and so on. (Again, the real tree is deeper than this -- there's more than just age and gender in the actual model. Also, age is specified at a more fine-grained level but the concept is the same.)

I retrieve these values from the generator function in batches and generate sample user profiles based on that.

So within a span like '18-24' I linearly distribute dates of birth by calculating (upper date bound - lower date bound) / count and using that as the delta to step the date of births by within the span starting from the lower date bound. (And of course the more fine-grained your spans are, the lesser the impact of linearly distributing values within each span.)

And I generate additional properties that are pseudo-random like usernames.

The script execution time is presently down to about 6 and a half minutes for generating and copying (using copy instead of insert was another optimization I made to the script along with a few others from https://www.postgresql.org/docs/current/populate.html after meticulously measuring that said optimizations had a significant positive impact on the time it took to put the data into the db), memory usage is very reasonable, and with the most recent commit I made which makes the generated usernames guaranteed to be unique (for up to a set number of generated user above the 10,000,000 I am doing), the stage is set for running the script on as many cores as your computer has to offer, further cutting down the script execution time.

Anyway, now on to how any of this is relevant to your comment.

I wrote this tool as part of my development process because a lot of the value that the project will offer to its users is tied directly to segmenting the users by various facets, and so I need sample data that allows me to explore the user experience while developing the project.

But I also think that this sort of tool could be useful in the situation that you guys are talking about here.

So when you are doing a migration you could generate a smaller test set of data that realistically reflects the real data of your users and you could then run the migration on that test set to get a very good idea about the cost of the various operations like you wanted. Agree?

Also, if anyone else knows of similar tools to mine I am always interested in knowing about them. Discovering what exists already can be hard and I have gotten to learn about many interesting and useful tools through discussions with others.