Assumptions
Filling in for missing information in the question:
Input data comes from a file on the database server.
File format matches COPY
output, with a unique id
per row to match the target table.
If not, use COPY
options to adapt to the format if possible, or else format the input properly.
You are updating most or all rows in the target table.
You can afford to drop and recreate the target table.
That means concurrent access (if any) is blocked for the time. Else consider:
There are no depending objects, except for indices. (Else you have to drop and recreate.)
You have plenty of free RAM and storage.
Solution
Go with a similar approach as outlined in the linked post. With major optimizations.
Simple & fast way to create the temporary table:
CREATE TEMP TABLE tbl_tmp AS SELECT * FROM tbl LIMIT 0;
A single big UPDATE
from a (temporary) table inside the database is typically faster than single-row updates from outside the database by orders of magnitude.
In PostgreSQL's MVCC model, an UPDATE
means to create a new row version and mark the old one as deleted. That's about as expensive as an INSERT
and a DELETE
combined (except for "toasted" values).
Plus, it creates a lot of dead tuples. When updating the whole table anyway, it's a lot faster to create a new table and drop the old one.
At the start of for the session, set temp_buffers
high enough to hold the temp table in RAM. To get an estimate how is needed, run a test with a small sample and use db object size functions:
SELECT pg_size_pretty(pg_relation_size('tbl_tmp')); -- complete size of table
SELECT pg_column_size(t) FROM tbl_tmp t LIMIT 10; -- size of sample rows
Without concurrent access
This allows for the simplest & fastest route.
SET temp_buffers = '1GB'; -- example value
CREATE TEMP TABLE tbl_tmp AS
SELECT * FROM tbl LIMIT 0;
COPY tbl_tmp FROM '/absolute/path/to/file.csv';
-- free up org. tbl name
ALTER TABLE tbl RENAME TO tbl_old;
CREATE TABLE tbl AS
SELECT t.col1, t.col2, u.field1, u.field2
FROM tbl_old t
JOIN tbl_tmp u USING (id);
-- free up org. index & constraint names
DROP TABLE tbl_old;
-- create indexes and constraints like in original table
ALTER TABLE tbl ADD PRIMARY KEY ...;
CREATE INDEX ... ON tbl (...);
CREATE INDEX ... ON tbl (...);
-- optional; dropped at end of session automatically
DROP TABLE tbl_tmp;
With concurrent access
SET temp_buffers = '1GB'; -- example value
CREATE TEMP TABLE tbl_tmp AS
SELECT * FROM tbl LIMIT 0;
COPY tbl_tmp FROM '/absolute/path/to/file.csv';
-- protect against concurrent writes - if applicable
-- reads are still allowed
LOCK TABLE tbl IN SHARE MODE;
CREATE TABLE tbl_new AS
SELECT t.col1, t.col2, u.field1, u.field2
FROM tbl t
JOIN tbl_tmp u USING (id);
-- Create indexes like in original table (with new names)
ALTER TABLE tbl_new ADD PRIMARY KEY ...;
CREATE INDEX ... ON tbl_new (...);
CREATE INDEX ... ON tbl_new (...);
-- exclusive lock on tbl for a very brief time!
DROP TABLE tbl;
ALTER TABLE tbl_new RENAME TO tbl;
-- optionally rename indexes and constraints to now free org. names.
DROP TABLE tbl_tmp;
Concurrent, blocked operations on the table will wait, once the table is locked near the end and fail as soon as the transaction is committed, because the table name is resolved to its OID immediately, but the new table has a different OID. The table stays consistent, but concurrent operations may get an exception and have to be repeated. See:
UPDATE
route
If you (have to) go the UPDATE
route, drop any index that is not needed during the update and recreate it afterwards. It is much cheaper to create an index from scratch than to update it incrementally. Plus, removing unneeded indexes may allow for HOT updates.
Related:
explain analyze
that it's using an index for the lookup?