使用int類型作為表的主鍵在pg中是很常見的情況,但是pg中int類型的范圍在-2147483648到2147483647,最大只有21億,這個在一些大表中很容易就會達到上限。一旦達到上限,那么表中便沒辦法在插入數據了,這個將會是很嚴重的問題。
如何監控?
對于此類情況,我們可以考慮將序列使用情況加入到監控中,防止達到最大值后表中無法插入數據的情況發生。
可以使用下面SQL去查詢庫中序列的使用情況:
SELECTseqs.relname AS sequence,format_type(s.seqtypid, NULL) sequence_datatype,CONCAT(tbls.relname, '.', attrs.attname) AS owned_by,format_type(attrs.atttypid, atttypmod) AS column_datatype,pg_sequence_last_value(seqs.oid::regclass) AS last_sequence_value,TO_CHAR((CASE WHEN format_type(s.seqtypid, NULL) = 'smallint' THEN(pg_sequence_last_value(seqs.relname::regclass) / 32767::float)WHEN format_type(s.seqtypid, NULL) = 'integer' THEN(pg_sequence_last_value(seqs.relname::regclass) / 2147483647::float)WHEN format_type(s.seqtypid, NULL) = 'bigint' THEN(pg_sequence_last_value(seqs.relname::regclass) / 9223372036854775807::float)END) * 100, 'fm9999999999999999999990D00%') AS sequence_percent,TO_CHAR((CASE WHEN format_type(attrs.atttypid, NULL) = 'smallint' THEN(pg_sequence_last_value(seqs.relname::regclass) / 32767::float)WHEN format_type(attrs.atttypid, NULL) = 'integer' THEN(pg_sequence_last_value(seqs.relname::regclass) / 2147483647::float)WHEN format_type(attrs.atttypid, NULL) = 'bigint' THEN(pg_sequence_last_value(seqs.relname::regclass) / 9223372036854775807::float)END) * 100, 'fm9999999999999999999990D00%') AS column_percent FROMpg_depend dJOIN pg_class AS seqs ON seqs.relkind = 'S'AND seqs.oid = d.objidJOIN pg_class AS tbls ON tbls.relkind = 'r'AND tbls.oid = d.refobjidJOIN pg_attribute AS attrs ON attrs.attrelid = d.refobjidAND attrs.attnum = d.refobjsubidJOIN pg_sequence s ON s.seqrelid = seqs.oid WHEREd.deptype = 'a'AND d.classid = 1259;
查詢出的結果類似這樣:
解決方案
1、修改序列為負數
因為pg中int類型是包含負數的,所以如果序列從0開始遞增即將達到最大值,那么可以考慮切換到負數排序。將序列的起始值設置為-1然后降序來遞增。
alter sequence test_id_seq no minvalue start with -1 increment -1 restart;
這種方式不需要修改表的結構,可以很快的解決問題。但是這種方案的前提是主鍵列只是單純用來做遞增的唯一約束用的,可以接受使用負數才行。
而且這也只能將可用數據范圍翻倍,只能短期解決問題,如果后續負數用完了那就沒辦法了,只能去修改字段類型了。
2、修改序列cycle屬性(分區表)
如果你的表是分區表的話,還可以考慮直接修改序列的屬性為cycle。因為在pg中,主鍵并不是全局性的約束,而只是針對單個分區的。
即分區1和分區2中都可以出現主鍵id相同的數據。當然,這種方案僅限于分區表的場景。
alter sequence test_id_seq cycle;
3、修改字段類型為bigint
如果上面倆種方案都沒法解決的話,那最終只能選擇修改字段類型為bigint的方式了。不過肯定也不能直接去這樣修改:
alter table xxx alter id type bigint;
畢竟一般int類型達到上限的表都很大了,修改int為bigint是會重寫表的,需要長時間持有獨占鎖,這個對業務來說是難以接受的。
比較推薦的方案就是新增一個bigint列,然后用其去替換原先的int列。
alter table test add column id_new bigint; CREATE UNIQUE INDEX CONCURRENTLY test_id_new ON test (id_new);
緊接著再創建對應的bigint的序列。
CREATE SEQUENCE test_id_new_seq START 2147483776 AS bigint; ALTER TABLE test ALTER COLUMN id_new SET DEFAULT nextval ('test_id_new_seq'); alter sequence test_id_new_seq owned by test.id_new;
現在舊的int類型和新的bigint類型的列就都在自增了。
bill=# select * from test;id | value | id_new ------------+-------+------------2000000009 | |2000000010 | |2000000011 | | 21474837762000000012 | | 2147483777
然后我們就可以將id列和id_new列進行重命名了,這一步需要放在同一個事務中去處理。
BEGIN; ALTER TABLE test DROP CONSTRAINT test_pkey; ALTER TABLE test ALTER COLUMN id DROP DEFAULT; ALTER TABLE test RENAME COLUMN id TO id_old; ALTER TABLE test RENAME COLUMN id_new TO id; ALTER TABLE test ALTER COLUMN id_old DROP NOT NULL; ALTER TABLE test ADD CONSTRAINT id_not_null CHECK (id IS NOT NULL) NOT VALID; COMMIT;
由于之前添加id_new列中有大量null值,因此需要將約束設置為NOT VALID,但是我們需要將該列變為主鍵的話,之前的null值還是需要去處理的,這里使用批量update的方式去進行更新。
WITH unset_values AS (SELECTid_oldFROMtestWHEREid IS NULLLIMIT 1000) UPDATEtest SETid = unset_values.id_old FROMunset_values WHEREunset_values.id_old = test.id_old;
null的數據處理完之后,我們便可以將新的id列更改為主鍵了。
ALTER TABLE test VALIDATE CONSTRAINT id_not_null; ALTER TABLE test ADD CONSTRAINT test_pkey PRIMARY KEY USING INDEX test_id_new; ALTER TABLE test DROP CONSTRAINT id_not_null;
最后我們便可以將舊的id列刪除了,記得刪除完之后重新收集下表的統計信息。
bill=# ALTER table test drop column id_old; ALTER TABLE bill=# analyze t1; ANALYZE bill=# \d testTable "public.test"Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------------------------------------value | integer | | |id | bigint | | not null | nextval('test_id_new_seq'::regclass) Indexes:"test_pkey" PRIMARY KEY, btree (id)
總的來說這種方式也是比較麻煩的,其中在線創建索引和批量update都是比較耗時的操作。
因此對于應用來說還是應該盡可能的避免出現這種情況,大表在設計的階段就應該考慮使用bigint或者bigserial來代替int類型,不要在int類型快要達到最大值再開始去救火。