Take the 2-minute tour ×
Database Administrators Stack Exchange is a question and answer site for database professionals who wish to improve their database skills and learn from others in the community. It's 100% free, no registration required.

We have a 2.2 GB table in Postgres with 7,801,611 rows in it. We are adding a uuid/guid column to it and I am wondering what the best way to populate that column is (as we want to add a NOTNULL to it).

If I understand Postgres correct an update is technically a delete and insert so this is basically rebuilding the entire 2.2 gb table. Also we have a slave running so we don't want that to lag behind

Is there any way better than writing a script that slowly populates it over time?

share|improve this question
1  
Have you already run an ALTER TABLE .. ADD COLUMN ... or is that part to be answered as well? –  ypercube Oct 30 '13 at 16:34
    
Have not run any table modifications yet, just in planning stage. I have done this before by adding the column, populating it, then adding the constraint or index. However, this table is significantly bigger and I am worried about the load, locking, replication, etc... –  Collin Peters Oct 30 '13 at 17:01
1  
2.2 gb is large? 7.8 million= Just add it. Finished. TRy that on a 8.5 billion row table I have (and actually am optimizing data type wise now). 2.2gb is something any non-trivial database server just buffers in memory. –  TomTom Oct 30 '13 at 21:22
    
in relatively recent (9.x something?) version there are deferred constraints that would allow you to add the not null without immediately having the whole thing populated. depending on what the column is you could then maybe lazily populate the old records if the uuid is not found. –  xenoterracide Oct 31 '13 at 9:07

2 Answers 2

up vote 12 down vote accepted

It very much depends on the details of your requirements.

If you have sufficient free space (at least 110% of pg_size_pretty((pg_total_relation_size(tbl))) on disk and can afford a share lock for some time and an exclusive lock for a very short time:

  • Create the new table including the uuid column with a column default.

    CREATE TABLE tbl_new (
      tbl_uuid uuid NOT NULL UNIQUE DEFAULT uuid_generate_v1()
     , ... -- rest of your columns 
    );
    

    I am using a function from the additional uuid-oss module here.

The rest goes into one transaction.

  • Lock the table against concurrent changes in SHARE mode (still allowing concurrent reads).

  • Copy the whole table while letting the column default in the new table populate the new column - possibly ordering the rows in some favorable fashion while being at it.
    If you are going to reorder rows while being at it, be sure to set work_mem as high as you can afford (just for your session, not globally).

  • Add constraints, foreign keys, indices, triggers etc. to new table. When updating large portions of a table it is much faster to create indices from scratch than to update iteratively.

  • When the new table is ready, drop the old and rename the new to make it a drop-in replacement. Only this last step acquires an exclusive lock on the old table for the rest of the transaction - which should be very short now.
    It also requires that you delete any object depending on the table type (views, functions using the table type in the signature, ...) and recreate them afterwards.

-- SET work_mem = '???? MB';

BEGIN;
LOCK TABLE tbl IN SHARE MODE;

INSERT INTO tbl_new (<list of existing columns>)
SELECT <list of existing columns>
FROM   tbl
-- ORDER BY ??
;

-- add constraints, indices, triggers, ...

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME tbl;

-- recreate views etc. if any

COMMIT;

-- RESET work_mem;

This should be fastest. Any other method of updating in place has to rewrite the whole table as well, just in a much more expensive fashion. You would only go that route if you don't have enough free space on disk or cannot afford locks on the whole table.

What happens to concurrent writes?

Other transaction (in other sessions) trying to INSERT / UPDATE / DELETE in the same table after your transaction has taken the SHARE lock, will wait until the lock is released or a timeout kicks in, whichever comes first. They will fail either way, since the table they were trying to write to has been deleted from under them.

The new table has a new table OID, but concurrent transaction have already resolved the table name to the OID of the previous table. When the lock is finally released, they try to lock the table themselves before writing to it and find that it's gone. Postgres will answer:

ERROR: could not open relation with OID 123456

Where 123456 is the OID of the old table.

If you cannot afford that to happen, you have to keep your original table.

Two alternatives keeping the existing table

  1. Update in place (possibly running the update on small segments at a time) before you add the NOT NULL constraint. Adding a new column with NULL values and without NOT NULL constraint is cheap.
    Since Postgres 9.2 you can also create a CHECK constraint with NOT VALID:.

    The constraint will still be enforced against subsequent inserts or updates

    That allows you to update rows peu à peu - in multiple separate transactions. This avoids keeping row locks for too long and it also allows dead rows to be reused. Finally, add the NOT NULL constraint and remove the NOT VALID CHECK constraint:

    ALTER TABLE tbl ADD CONSTRAINT tbl_no_null
    CHECK (tbl_uuid IS NOT NULL) NOT VALID;
    
    -- update rows in multiple batches in separate transactions
    
    ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL;
    ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;
    
  2. Prepare the new state in a temporary table, TRUNCATE the original and refill from the temp table. All in one transaction. You still need to take a SHARE lock before preparing the new table to prevent losing concurrent writes.

    Details in this related answer on SO or this one.

share|improve this answer
    
Fantastic answer! Exactly the info I was looking for. Two questions 1. Do you have any idea on an easy way to test how long an action like this would take? 2. If it takes say 5 minutes, what happens to actions trying to update a row in that table during those 5 minutes? –  Collin Peters Nov 4 '13 at 17:51
    
@CollinPeters: 1. The lion's share of the time would go into copying the big table - and possibly recreating indices and constraints (that depends). Dropping and renaming is cheap. To test you can run your prepared SQL script without the LOCK up to and excluding the DROP. I could only utter wild and useless guesses. As for 2., please consider the addendum to my answer. –  Erwin Brandstetter Nov 4 '13 at 19:04

I don't have a "best" answer, but I have a "least bad" answer that might let you get things done reasonably fast.

My table had 2MM rows and the update performance was chugging when I tried to add a secondary timestamp column that defaulted to the first.

ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;

After it hung for 40 minutes, I tried this on a small batch to get an idea of how long this could take -- the forecast was around 8 hours.

The accepted answer is definitely better -- but this table is heavily used in my database. There are a few dozen tables that FKEY onto it; I wanted to avoid switching FOREIGN KEYS on so many tables. And then there are views.

A bit of searching docs, case-studies and StackOverflow, and I had the "A-Ha!" moment. The drain wasn't on the core UPDATE, but on all the INDEX operations. My table had 12 indexes on it -- a few for unique constraints, a few for speeding up the query planner, and a few for fulltext search.

Every row that was UPDATED wasn't just working on a DELETE/INSERT, but also the overhead of altering each index and checking constraints.

My solution was to drop every index and constraint, update the table, then add all the indexes/constraints back in.

It took about 3 minutes to write a SQL transaction that did the following:

  • BEGIN;
  • dropped indexes/constaints
  • update table
  • re-add indexes/constraints
  • COMMIT;

The script took 7 minutes to run.

The accepted answer is definitely better and more proper... and virtually eliminates the need for downtime. In my case though, it would have taken significantly more "Developer" work to use that solution and we had a 30 minute window of scheduled downtime that it could be accomplished in. Our solution addressed it in 10.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.