Memadatkan urutan dalam PostgreSQL

9

Saya memiliki id serial PRIMARY KEYkolom di tabel PostgreSQL. Banyak idyang hilang karena saya telah menghapus baris yang sesuai.

Sekarang saya ingin "memadatkan" tabel dengan me-restart urutan dan menugaskan ulang idsedemikian rupa sehingga idurutan asli dipertahankan. Apa itu mungkin?

Contoh:

  • Sekarang:

 id | data  
----+-------
  1 | hello
  2 | world
  4 | foo
  5 | bar
  • Setelah:

 id | data  
----+-------
  1 | hello
  2 | world
  3 | foo
  4 | bar

Saya mencoba apa yang disarankan dalam jawaban StackOverflow , tetapi tidak berhasil:

# alter sequence t_id_seq restart;
ALTER SEQUENCE
# update t set id=default;
ERROR:  duplicate key value violates unique constraint t_pkey
DETAIL:  Key (id)=(1) already exists.
rubik
sumber

Jawaban:

9

Pertama, celah dalam suatu urutan harus diharapkan. Tanyakan pada diri Anda apakah Anda benar-benar perlu menghapusnya. Hidup Anda menjadi lebih sederhana jika Anda hanya hidup dengannya. Untuk mendapatkan angka tanpa celah, alternatif (seringkali lebih baik) adalah menggunakan VIEWdengan row_number(). Contoh dalam jawaban terkait ini:

Berikut adalah beberapa resep untuk menghilangkan celah.

1. Baru, meja murni

Hindari komplikasi dengan pelanggaran unik dan mengasapi meja dan cepat . Hanya untuk kasus sederhana di mana Anda tidak terikat oleh referensi FK, tampilan di atas meja atau objek bergantung lainnya, atau dengan akses bersamaan. Lakukan dalam satu transaksi untuk menghindari kecelakaan:

BEGIN;
LOCK tbl;

CREATE TABLE tbl_new (LIKE tbl INCLUDING ALL);

INSERT INTO tbl_new -- no target list in this case
SELECT row_number() OVER (ORDER BY id), data  -- all columns in default order
FROM   tbl;

ALTER SEQUENCE tbl_id_seq OWNED BY tbl_new.id;  -- make new table own sequence

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME TO tbl;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

COMMIT;

CREATE TABLE tbl_new (LIKE tbl INCLUDING ALL)menyalin struktur termasuk kendala dan default dari tabel asli. Kemudian buat kolom tabel baru memiliki urutan:

Dan reset ke maksimum baru:

Ini membawa keuntungan bahwa meja baru ini bebas dari bloat dan bergerombol id.

2. UPDATEdi tempat

Ini menghasilkan banyak baris mati dan membutuhkan (otomatis) VACUUMnanti.

Jika serialkolom juga merupakan PRIMARY KEY(seperti dalam kasus Anda) atau memiliki UNIQUEkendala, Anda harus menghindari pelanggaran unik dalam proses. Default (lebih murah) untuk batasan PK / UNIQUE adalah NOT DEFERRABLE, yang memaksa cek setelah setiap baris tunggal. Semua detail di bawah pertanyaan terkait ini di SO:

Anda dapat mendefinisikan batasan Anda sebagai DEFERRABLE(yang membuatnya lebih mahal).
Atau Anda dapat menghilangkan batasan dan menambahkannya kembali setelah Anda selesai:

BEGIN;

LOCK tbl;

ALTER TABLE tbl DROP CONSTRAINT tbl_pkey;  -- remove PK

UPDATE tbl t  -- intermediate unique violations are ignored now
SET    id = t1.new_id
FROM  (SELECT id, row_number() OVER (ORDER BY id) AS new_id FROM tbl) t1
WHERE  t.id = t1.id;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

ALTER TABLE tbl ADD CONSTRAINT tbl_pkey PRIMARY KEY(id); -- add PK back

COMMIT;

Tidak ada yang mungkin saat Anda memilikiFOREIGN KEYkendala referensi kolom karena ( per dokumentasi ):

Kolom yang direferensikan harus berupa kolom dari batasan kunci primer atau unik yang tidak dapat ditangguhkan dalam tabel yang direferensikan.

Anda perlu (mengunci semua tabel yang terlibat dan) menjatuhkan / membuat ulang batasan FK dan memperbarui semua nilai FK secara manual (lihat opsi 3. ). Atau Anda harus memindahkan nilai dari satu detik dengan yang lain UPDATEuntuk menghindari konflik. Misalnya, dengan asumsi Anda tidak memiliki angka negatif:

BEGIN;
LOCK tbl;

UPDATE tbl SET id = id * -1;  -- avoid conflicts

UPDATE tbl t
SET    id = t1.new_id
FROM  (SELECT id, row_number() OVER (ORDER BY id DESC) AS new_id FROM tbl) t1
WHERE  t.id = t1.id;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

COMMIT;

Kekurangannya seperti disebutkan di atas.

3. Temp table TRUNCATE,,INSERT

Satu lagi opsi jika Anda memiliki banyak RAM. Ini menggabungkan beberapa keunggulan dari dua cara pertama. Hampir secepat opsi 1. dan Anda mendapatkan tabel baru yang murni tanpa mengasapi tetapi tetap menggunakan semua kendala dan dependensi seperti pada opsi 2.
Namun , per dokumentasi:

TRUNCATE tidak dapat digunakan pada tabel yang memiliki referensi kunci asing dari tabel lain, kecuali semua tabel tersebut juga terpotong dalam perintah yang sama. Memeriksa validitas dalam kasus-kasus seperti itu akan membutuhkan pemindaian tabel, dan intinya bukan untuk melakukan satu.

Penekanan berani saya.

Anda bisa menghilangkan batasan FK untuk sementara dan menggunakan CTE pemodifikasi data untuk memperbarui semua kolom FK:

SET temp_buffers = 500MB;   -- example value, see 1st link below

BEGIN;

CREATE TEMP TABLE tbl_tmp AS
SELECT row_number() OVER (ORDER BY id) AS new_id, *
FROM   tbl
ORDER  BY id;  -- order here to use index (if one exists)

-- drop FK constraints in other tables referencing this one
-- which takes out an exclusive lock on those tables

TRUNCATE tbl;

INSERT INTO tbl
SELECT new_id, data  -- list all columns in order
FROM tbl_tmp;        -- rely on established order in tbl_tmp
-- ORDER BY id;      -- only to be absolutely sure (not necessary)

--  example for table "fk_tbl" with FK column "fk_id"
UPDATE fk_tbl f
SET    fk_id = t.new_id  -- set to new ID
FROM   tbl_tmp t
WHERE  f.fk_id = t.id;   -- match on old ID

-- add FK constraints in other tables back

COMMIT;

Terkait, dengan rincian lebih lanjut:

Erwin Brandstetter
sumber
Jika semua FOREIGN KEYSdiatur ke CASCADEtidak bisa Anda hanya mengulang kunci utama lama dan memperbarui nilainya di tempat (dari nilai lama ke yang baru)? Pada dasarnya ini adalah opsi 3 tanpa TRUNCATE tbl, mengganti INSERTdengan UPDATE, dan tanpa perlu memperbarui kunci asing secara manual.
Gili
@ Geili: Anda bisa , tetapi loop seperti itu sangat mahal. Karena Anda tidak dapat memperbarui seluruh tabel sekaligus karena pelanggaran kunci unik dalam indeks, Anda memerlukan UPDATEperintah terpisah untuk setiap baris. Lihat penjelasan di ② atau coba dan lihat sendiri.
Erwin Brandstetter
Saya tidak berpikir bahwa kinerja adalah masalah dalam kasus saya. Cara saya melihatnya, ada dua jenis algoritma: yang "menghentikan dunia" dan yang berjalan dengan diam-diam di latar belakang tanpa harus menurunkan server. Dengan asumsi bahwa pemadatan hanya terjadi sekali dalam bulan biru (misalnya ketika mendekati batas atas tipe data), sebenarnya tidak ada batas atas pada jumlah waktu yang harus diambil. Selama kita memadatkan catatan lebih cepat daripada yang baru ditambahkan, kita harus baik-baik saja.
Gili