导入期间延后建立索引与约束
在批量导入前后删除并重建索引和约束,大幅减少写放大。
导入期间延后建立索引与约束 是 CoddyKit 上的免费 PostgreSQL Performance & Query Optimization 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PostgreSQL Performance & Query Optimization 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PostgreSQL Performance & Query Optimization 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Bulk Loads Get Slow
When you load millions of rows into a table that already has indexes and constraints, PostgreSQL pays a hidden tax on every single row.
- Each index must be updated (B-tree page splits, WAL writes).
- Each foreign key triggers a lookup against the referenced table.
- Each unique/check constraint is validated per-row.
This per-row work is called write amplification: one logical INSERT becomes many physical writes. The core optimization in this lesson is to defer that work — load the raw data first, then build indexes and validate constraints once, in bulk.
The Per-Index Cost
Maintaining a B-tree index during a load is not free. For each inserted row, PostgreSQL must walk the tree, find the leaf page, possibly split it, and log the change to WAL.
Building the same index after the data is present is far cheaper: PostgreSQL sorts all keys at once and writes dense, sequential pages. A table with 5 indexes loaded row-by-row does roughly 6x the write work versus loading the heap alone.
The takeaway: fewer indexes present during load = less amplification.
Pattern: Drop, Load, Rebuild
The classic ETL pattern for a table that will receive a large load is:
- Drop the secondary indexes.
- Load the data (COPY is fastest).
- Rebuild the indexes in one pass.
Below is the skeleton. Note we keep the primary key for now and only drop secondary indexes that are not needed during the load itself.
DROP INDEX idx_orders_customer_id;
DROP INDEX idx_orders_created_at;
COPY orders FROM '/data/orders.csv' WITH (FORMAT csv, HEADER true);
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_orders_created_at ON orders (created_at);COPY Beats INSERT for Loading
Once indexes are out of the way, the loading method matters. COPY streams rows in a single command with minimal per-row overhead, while thousands of individual INSERT statements each pay parse, plan, and round-trip costs.
For ETL throughput, prefer COPY (or \copy from psql) over row-at-a-time inserts. If you must use INSERT, batch many rows per statement.
COPY staging_events (user_id, event_type, payload, created_at)
FROM '/data/events.csv'
WITH (FORMAT csv, HEADER true);Deferring Foreign Key Validation
Foreign keys are validated per-row during a load, doing an index lookup on the parent table each time. You can avoid this by making the constraint NOT VALID first, loading, then validating in bulk.
ADD CONSTRAINT ... NOT VALID adds the FK without checking existing rows. New rows are still checked on insert, so to truly skip per-row work you drop and re-add after load, or load before adding the FK.
-- Add the FK without scanning existing rows
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers (id)
NOT VALID;
-- Later, validate all rows in one bulk pass
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_customer;Why NOT VALID Then VALIDATE Helps
Adding a foreign key the normal way takes an ACCESS EXCLUSIVE lock and scans the whole table while blocking writes. The two-step approach splits this:
- ADD ... NOT VALID is fast and takes a brief strong lock only to record the constraint.
- VALIDATE CONSTRAINT scans the table under a weaker
SHARE UPDATE EXCLUSIVElock, allowing concurrent reads and writes.
For loads, this means you do the expensive validation once, after all data is present, rather than per-row.
DEFERRABLE Constraints Within a Transaction
PostgreSQL also supports DEFERRABLE constraints, which postpone checking until the end of a transaction (COMMIT). This is different from dropping a constraint: the check still runs, just later.
It is useful when rows arrive in an order that temporarily violates an FK or unique constraint (for example, child rows before parents within one transaction).
ALTER TABLE order_items
ADD CONSTRAINT fk_items_order
FOREIGN KEY (order_id) REFERENCES orders (id)
DEFERRABLE INITIALLY DEFERRED;
BEGIN;
-- insert children and parents in any order;
-- FK is checked only at COMMIT
COMMIT;Deferred Check vs Dropped Constraint
Be clear on the trade-off:
- DEFERRABLE INITIALLY DEFERRED still validates every row, just at COMMIT instead of at INSERT. It fixes ordering problems but does not remove the validation cost.
- Drop / re-add (or NOT VALID + VALIDATE) removes per-row work entirely and revalidates in one efficient scan.
For maximum throughput on huge loads, dropping and rebuilding wins. For correctness with tricky insert ordering, deferrable is the right tool.
Tuning the Index Rebuild
Rebuilding indexes after a load is itself a sort-heavy operation. Two settings make it much faster for the session running the load:
maintenance_work_mem— more memory means fewer external sort merges when building indexes.max_parallel_maintenance_workers— lets a single CREATE INDEX use multiple CPUs.
Raise these for the load session, then build the indexes.
SET maintenance_work_mem = '2GB';
SET max_parallel_maintenance_workers = 4;
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_orders_created_at ON orders (created_at);A Complete ETL Sequence
Putting it together for a large incremental load into an existing table, a robust order of operations is:
- Drop secondary indexes.
- Drop or disable expensive foreign keys.
- Raise
maintenance_work_mem. - Load via COPY.
- Rebuild indexes.
- Re-add FKs and VALIDATE.
- Run ANALYZE so the planner has fresh statistics.
ALTER TABLE orders DROP CONSTRAINT fk_orders_customer;
DROP INDEX idx_orders_created_at;
SET maintenance_work_mem = '1GB';
COPY orders FROM '/data/orders.csv' WITH (FORMAT csv, HEADER true);
CREATE INDEX idx_orders_created_at ON orders (created_at);
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers (id);
ANALYZE orders;Don't Forget ANALYZE
After a big load, the table statistics the planner relies on are stale — it may still think the table is tiny. That leads to bad plans (sequential scans where an index would win, or wrong join orders).
Always run ANALYZE (or VACUUM ANALYZE) on freshly loaded tables before running queries against them. Rebuilding indexes does not update planner statistics; only ANALYZE does.
ANALYZE orders;
-- or to also reclaim space and freeze:
VACUUM ANALYZE orders;Quick Check
Test your understanding of the throughput trade-offs.
Recap
Key takeaways for deferring indexes and constraints during bulk loads:
- Live indexes and constraints cause write amplification — one INSERT becomes many physical writes.
- The winning pattern is drop, load, rebuild: remove secondary indexes and FKs, load with
COPY, then recreate them in one pass. ADD CONSTRAINT ... NOT VALIDfollowed byVALIDATE CONSTRAINTmoves FK checking out of the per-row path and into a single bulk scan under a lighter lock.DEFERRABLE INITIALLY DEFERREDonly postpones checks to COMMIT — it fixes insert-ordering issues but does not eliminate validation cost.- Raise
maintenance_work_memandmax_parallel_maintenance_workersto speed up the rebuild. - Always finish with
ANALYZEso the planner sees the new data.
常见问题解答
「导入期间延后建立索引与约束」课时是免费的吗?
是的 — 「导入期间延后建立索引与约束」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PostgreSQL Performance & Query Optimization 课程的其余内容,请升级到 CoddyKit PRO。 PostgreSQL Performance & Query Optimization 课程共包含 4 节课。
「导入期间延后建立索引与约束」这节课中我会学到什么?
在批量导入前后删除并重建索引和约束,大幅减少写放大。 你通过在浏览器中直接运行的动手代码来练习 PostgreSQL Performance & Query Optimization,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PostgreSQL Performance & Query Optimization 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PostgreSQL Performance & Query Optimization 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「导入期间延后建立索引与约束」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PostgreSQL Performance & Query Optimization 课中编写并运行代码吗?
能。每节 PostgreSQL Performance & Query Optimization 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。