如何在PostgreSQL中更新大表
- 2019 年 11 月 6 日
- 筆記
本文來源:www.codacy.com/blog/how-to…
在Postgres中更新大型表並不像看起來那樣簡單。如果您的表包含數億行,您將發現很難及時進行簡單的操作,例如添加列或更改列類型。
在不停機的情況下進行這類操作是一個更大的挑戰。在這篇博客文章中,我將嘗試概述一些策略,以在管理大型數據集的同時最大程度地減少表不可用性。
一般準則
當您更新列中的值時,Postgres將在磁盤中寫入一個新行,棄用舊行,然後繼續更新所有索引。此過程等同於INSERT
加上每一行後再DELETE
,這會佔用大量資源。
除此之外,需要更新大表時還應了解的事項列表:
- 從頭開始創建新表比更新每一行要快。順序寫比稀疏更新快,並且最後不會出現死行。
- 表約束和索引嚴重延遲了每次寫入。如果可能,應在更新運行時刪除所有索引,觸發器和外鍵,並在最後重新創建它們。
- 添加沒有默認值的可空列是一種廉價的操作。寫入列的實際數據是昂貴的部分。
- 更新行時,不會重寫存儲在TOAST中的數據
- 從Postgres 9.2開始,在某些數據類型之間進行轉換不需要重寫整個表。例如:從VARCHAR(32)轉換為VARCHAR(64)。
考慮到這一點,讓我們看一些可以用來有效更新表中大量數據行的策略:
增量更新
如果您可以使用例如順序ID對數據進行細分,則可以批量更新行。由於您只需要保持較短時間的鎖定,因此可以最大化表的可用性。如果添加新列,則可以將其臨時設置為可為空,然後開始逐漸用新值填充它。
這種方法的主要問題是性能,這是一個非常緩慢的過程,因為就地更新成本很高。在遷移期間,它可能還需要更複雜的應用程序邏輯。
創建一個新表
更新大表的最快方法是創建一個新表。
如果可以安全地刪除現有表,並且有足夠的磁盤空間,則執行更新的最簡單方法是將數據插入到新表中,然後對其進行重命名。以下是此操作的基本執行腳本:
create table user_info_copy (LIKE user_info INCLUDING INDEXES INCLUDING COMMENTS); INSERT INTO user_info_copy SELECT user_no, idcard_no, real_name, bankcard_no, bind_mobile , false, bind_status, user_identity, create_time, creator , edit_time, editor, is_del, VERSION, customer_id , id_card_type, source_id, platform_no, one_passport_no, bank_code FROM user_info; drop TABLE user_info; alter table user_info_copy rename to user_info;
重新創建現有表
如果由於不想重新創建視圖或由於其他限制而不能刪除原始表,則可以使用臨時表保存新值,截斷舊錶並在那裡重寫數據。當您有未決的寫請求時,此方法也有一些優點,如我們將在下一部分中看到的。
如果您的表可以容納在內存中,則應在此事務期間增加temp_buffers
屬性。使用RAM代替磁盤來存儲臨時表將明顯提高性能:
SET temp_buffers = 3000MB; ----相應地更改此值
# 創建臨時表 CREATE TABLE temp_user_info( user_no BIGINT, PRIMARY KEY( user_no ) ); # 如果需要提速可以從表中刪除索引 # 複製數據到臨時表中 insert into temp_user_info select user_no from user_info; # 改變表結構,比如需要添加新列 TRUNCATE user_no; # 執行插入列字段語句 # 再把數據反寫到user_info表
處理並發寫入
即使進行了上述優化,重新創建表仍然是緩慢的操作。如果您正在實時數據庫中運行查詢,則可能需要處理並發寫入請求。
最簡單的方法是在事務期間在表上強制使用SHARE LOCK
, 語句如下
LOCK TABLE user_info IN SHARE MODE;
如果花費太長時間,所有寫請求將一直等到鎖釋放或超時為止。如果未刪除原始表,則一旦事務結束,將執行未超時的請求。請注意,即使使用相同的名稱創建新表,請求仍將失敗,因為它們使用表OID。
根據寫請求的性質,您還可以創建自定義規則來存儲對錶所做的更改。例如,您可以設置一個規則,以在開始數據遷移之前記錄已刪除的行:
CREATE RULE deleted_rule AS ON DELETE TO tbl DO INSERT INTO tbl_deletes VALUES ( OLD.id );
遷移結束時,您只需從tbl_deletes中讀取ID,然後在新表上將其刪除。可以使用類似的方法來處理其他類型的請求。
結論
一旦達到一定大小,曾經瞬時的操作可能需要幾個小時來準備和執行。個人實驗結論:
- 用存儲過程批量更新 560w , 1455秒結束
- 用複製表改名方法操作 560w數據, 120秒左右就結束了;