Verifikasi skema PostgreSQL sebelum migrasi pertama
Verifikasi skema PostgreSQL menemukan pemetaan yang salah, constraint lemah, indeks yang hilang, dan perubahan tidak aman sebelum migrasi pertama menyentuh data.

Builder AI dapat menghasilkan PostgreSQL yang valid, tetapi tetap menyimpulkan basis data yang keliru. Sintaks adalah bagian yang mudah. Kesalahan berbahaya sering terlihat masuk akal: relasi opsional menjadi wajib, string status mendapat constraint CHECK yang tidak lengkap, penghapusan berantai ke catatan yang seharusnya tetap ada, atau migrasi membuat ulang tabel dan diam-diam kehilangan kolom.
Karena itu, verifikasi skema PostgreSQL harus menguji makna, perilaku migrasi, dan pemulihan sebagai tiga hal terpisah. Saya hanya menyetujui skema hasil inferensi setelah melewati dataset yang diketahui, invariant yang eksplisit, kueri representatif, peninjauan perubahan destruktif, dan latihan pemulihan. Jika salah satunya tidak ada, migrasi masih sebatas usulan.
Migrasi pertama layak diperiksa sedetail ini, bahkan saat basis data produksi masih kosong. Kesalahan skema awal cepat mengeras karena kode aplikasi, data seed, laporan, dan migrasi berikutnya mulai bergantung padanya. Peninjauan lima belas menit sebelum eksekusi pertama biasanya lebih murah daripada menjelaskan enam bulan kemudian mengapa dua konsep berbeda memakai satu kolom teks nullable.
Skema hasil inferensi adalah spesifikasi yang belum tepercaya
Anggap skema hasil inferensi sebagai draf spesifikasi, bukan kebenaran yang siap dieksekusi. Builder melihat prompt, contoh layar, catatan impor, atau kode aplikasi yang dibuat. Builder tidak menyaksikan setiap pengecualian bisnis, aturan retensi, impor massal, perbaikan oleh dukungan, dan pembayaran gagal yang kelak akan disimpan basis data.
Mulailah dengan memisahkan tiga pertanyaan yang sering dicampur tim. Kebenaran skema menanyakan apakah tabel dan constraint memodelkan domain. Keamanan migrasi menanyakan apakah operasi yang diusulkan menjaga data yang ada dan membuat basis data tetap dapat digunakan saat operasi berjalan. Kesiapan pemulihan menanyakan apakah Anda dapat kembali ke keadaan yang diketahui setelah perubahan parsial atau perubahan yang secara semantik salah. Lolos dalam satu hal belum banyak bicara tentang dua hal lainnya.
Pernyataan CREATE TABLE dapat menggambarkan struktur akhir yang dimaksud, tetapi tetap dicapai melalui operasi yang tidak aman. Misalkan builder mengubah customer_name text menjadi customer_id bigint. Foreign key akhirnya mungkin masuk akal, tetapi migrasi yang menghapus kolom nama sebelum mencocokkan nama historis dengan pelanggan menghancurkan satu-satunya bukti yang diperlukan untuk pencocokan itu. Peninjauan skema menyetujui tujuan, peninjauan migrasi memeriksa perjalanannya.
Baca model yang diusulkan dengan lantang dalam bahasa domain. Katakan bahwa setiap faktur milik tepat satu pelanggan legal, alih-alih mengatakan invoices.customer_id mereferensikan customers.id. Kalimat pertama mengundang keberatan yang berguna: draf mungkin ada sebelum pelanggan dipilih, faktur hasil impor mungkin merujuk pelanggan yang diarsipkan, dan catatan legal mungkin perlu menyimpan nama pelanggan saat diterbitkan. Kosakata SQL dapat menyembunyikan perbedaan pendapat tersebut.
Saya meminta catatan asumsi di samping setiap tabel hasil inferensi. Catatan itu perlu menjelaskan arti satu baris, cara baris diidentifikasi, siapa pemiliknya, apakah baris dapat ada tanpa induk yang tampak, dan arti penghapusan. Jika tim tidak dapat menjawab hal-hal itu, builder telah menebak basis data yang belum dirancang tim.
Catatan yang diketahui mengungkap pemetaan tabel yang salah
Dataset yang diketahui harus berisi catatan yang dipilih untuk cakupan semantik, karena sampel acak yang besar sering mengulang kasus mudah yang sama. Sepuluh catatan yang dipilih dengan cermat dapat mengungkap lebih banyak daripada sepuluh ribu baris jalur sukses yang hampir sama.
Buat matriks pemetaan sebelum menjalankan DDL. Satu baris matriks perlu melacak setiap konsep sumber hingga tujuan yang diusulkan dan mencatat jumlah atau nilai yang diharapkan. Untuk aplikasi pesanan, artefaknya dapat terlihat seperti ini:
| Fakta yang diketahui | Tujuan yang diusulkan | Hasil yang diharapkan |
|---|---|---|
| Pesanan A memiliki dua item baris | orders dan order_items | Satu baris pesanan dan dua baris anak |
| Pesanan B belum memiliki akun | orders.account_id | Satu baris dengan akun NULL |
| Dua orang memakai email yang sama | contacts.email | Kedua baris tetap ada kecuali keunikan adalah aturan yang dinyatakan |
| Kode produk memiliki nol di depan | products.code | Nilai teks 00417 tetap tidak berubah |
| Pesanan yang dibatalkan mempertahankan tagihan | orders dan charges | Baris tagihan tetap ada setelah pembatalan |
Ini menangkap kesalahan pemetaan tabel sebelum detail constraint mengalihkan perhatian peninjauan. Builder AI sering menormalkan objek berulang menjadi tabel terpisah, dan itu biasanya masuk akal, tetapi pengulangan tidak membuktikan identitas. Dua alamat pengiriman dengan teks sama mungkin merupakan snapshot historis, bukan referensi ke satu baris alamat yang dapat diedit. Menggabungkannya berarti pengeditan alamat berikutnya menulis ulang sejarah.
Kesalahan sebaliknya juga terjadi. Builder dapat menyalin bidang pelanggan ke setiap pesanan karena layar menampilkannya bersama-sama. Sebagian nilai milik pelanggan, sementara sebagian lain harus tetap menjadi snapshot pesanan. Desain yang tepat dapat memuat customer_id dan bidang dokumen yang diterbitkan seperti billing_name. Menyebutnya duplikasi lalu menghapus salah satu sisi akan menghilangkan identitas saat ini atau kebenaran historis.
Muat dataset yang diketahui ke basis data sekali pakai memakai jalur impor atau seed yang sama dengan yang akan dipakai aplikasi. Lalu tulis assertion terhadap fakta, bukan sekadar jumlah baris:
SELECT
(SELECT count(*) FROM orders WHERE external_id = 'ORDER-A') AS order_a,
(SELECT count(*) FROM order_items i
JOIN orders o ON o.id = i.order_id
WHERE o.external_id = 'ORDER-A') AS order_a_items,
(SELECT account_id IS NULL FROM orders
WHERE external_id = 'ORDER-B') AS order_b_unassigned;
Bentuk hasil yang lolos harus eksplisit:
order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
1 | 2 | t
Jangan menerima perbedaan yang tidak dapat dijelaskan hanya karena aplikasi yang dibuat tetap merender. UI dapat menyembunyikan induk duplikat, anak yang hilang, kode yang terpotong, dan default yang dibuat-buat. Rekonsiliasikan setiap fixture yang disengaja sebelum membahas peluncuran produksi.
Constraint harus menyandikan kebenaran domain
Constraint basis data harus menolak keadaan yang selalu tidak valid, apa pun layar, API, impor, atau skrip perbaikan yang menulis baris tersebut. Jika aturan memiliki pengecualian atau bergantung pada fakta eksternal yang berubah, memaksakannya ke constraint sederhana sering menghasilkan pekerjaan yang terblokir atau data yang tidak jujur.
Primary key mengidentifikasi baris, tetapi tidak otomatis memberi identitas bisnis yang bermakna. ID bigint internal dapat berdampingan dengan nomor pesanan unik dalam cakupan tenant. Jika bisnis menyatakan nomor pesanan unik per tenant, UNIQUE (tenant_id, order_number) menyatakan aturan itu. Constraint unik global akan menolak catatan yang sah, sementara tanpa constraint akan memungkinkan ambiguitas saat retry.
Constraint CHECK cocok untuk fakta baris yang stabil seperti quantity > 0 atau finished_at >= started_at. Manual PostgreSQL menjelaskan bahwa basis data menganggap ekspresi CHECK tidak berubah selama masa berlaku constraint. Karena itu, CHECK yang memanggil fungsi dengan perilaku yang kemudian berubah dapat membuat baris lama melanggar aturan yang tampak berlaku. Gunakan ekspresi tetap untuk kebenaran yang tetap. Letakkan kebijakan yang berubah, seperti daftar yang saat ini diizinkan dan dikendalikan administrator, di tabel referensi atau alur kerja aplikasi.
Constraint status yang dibuat otomatis perlu dicurigai. Builder mungkin memeriksa contoh saat ini lalu menghasilkan:
status text NOT NULL
CHECK (status IN ('draft', 'active', 'closed'))
Itu benar hanya jika semua status tersebut lengkap dan tahan lama. Tanyakan tentang catatan gagal, dibatalkan, ditangguhkan, diimpor, dan catatan lama yang tidak dikenal. Jika state machine masih berubah, tabel lookup dapat membuat penambahan menjadi eksplisit, tetapi tidak menggantikan validasi transisi. Baris yang diizinkan berisi closed tidak mengatakan apa pun tentang apakah baris itu boleh langsung berpindah dari draft ke closed.
Gunakan keunikan dengan sengaja. PostgreSQL menerapkan constraint unik dengan indeks B-tree unik, tetapi indeks unik parsial menyatakan aturan berbeda. Soft delete umumnya memerlukan keunikan hanya di antara baris aktif:
CREATE UNIQUE INDEX users_tenant_email_live_uq
ON users (tenant_id, lower(email))
WHERE deleted_at IS NULL;
Ini tidak setara dengan UNIQUE (tenant_id, email, deleted_at). PostgreSQL memperlakukan nilai NULL sesuai aturan keunikannya, dan menambahkan cap waktu penghapusan mengubah identitas yang ditegakkan. Tinjau kasus duplikat yang tepat dengan fixture, jangan menyimpulkan perilaku dari daftar kolom saja.
Nullability adalah keputusan bisnis
Tetapkan kolom sebagai NOT NULL hanya jika domain mewajibkan nilai untuk setiap baris yang sah dan setiap jalur penulisan dapat menyediakannya. Desain layar adalah bukti yang lemah. Bidang wajib di formulir saat ini tidak mengatakan apa pun tentang impor, draf, baris yang dibuat sistem, atau catatan historis.
Tinjau empat keadaan secara terpisah: sumber menghilangkan bidang, sumber secara eksplisit mengirim null, sumber mengirim nilai kosong, dan sumber memberi nilai yang bermakna. API JSON, formulir, impor CSV, dan PostgreSQL dapat menangani keadaan ini secara berbeda. Jika aplikasi menyatukan keempatnya sebelum penyisipan, peninjauan skema harus mengungkap keputusan itu, bukan berpura-pura basis data telah menyelesaikannya.
Default memerlukan perhatian yang sama. Default memberi nilai saat INSERT menghilangkan kolom, tetapi tidak memperbaiki NULL yang eksplisit dan tidak membuktikan nilai itu benar. country_code DEFAULT 'US' berbahaya jika negara tidak diketahui mungkin terjadi. Baris itu kini berisi kebohongan yang yakin dan dapat dipercaya laporan serta logika kepatuhan.
Migrasi yang sering dibuat menambahkan kolom wajib dalam satu pernyataan:
ALTER TABLE customers
ADD COLUMN account_type text NOT NULL DEFAULT 'standard';
Pernyataan itu mungkin berjalan, tetapi setiap pelanggan historis berubah menjadi standar tanpa bukti. Urutan yang lebih aman menambahkan kolom nullable, menurunkan nilai dari data yang diketahui, mengukur baris yang belum terselesaikan, mencegah penghilangan baru dalam penulisan aplikasi, lalu baru menambahkan NOT NULL jika domain mendukungnya. Jika nilai tidak diketahui tetap sah, pertahankan NULL dan tetapkan cara kueri serta antarmuka menampilkannya.
PostgreSQL menyediakan pemisahan yang berguna untuk beberapa constraint. CHECK atau foreign key dapat ditambahkan sebagai NOT VALID, yang menghindari validasi semua baris lama saat dibuat, lalu diperiksa kemudian dengan VALIDATE CONSTRAINT. Manual mendokumentasikan ini sebagai cara menunda pemindaian tabel awal. Ini bukan izin untuk mengabaikan pelanggaran lama: penulisan baru tetap ditegakkan, sementara langkah validasi tetap harus lolos sebelum persetujuan.
Sebelum memperketat nullability, jalankan kueri distribusi yang menunjukkan kategori aktual:
SELECT
count(*) AS total,
count(*) FILTER (WHERE account_type IS NULL) AS nulls,
count(*) FILTER (WHERE account_type = '') AS empty_strings,
count(*) FILTER (WHERE account_type NOT IN
('standard', 'partner', 'internal')) AS unexpected
FROM customers;
Default yang dibuat AI dapat membuat kueri ini tampak bersih setelah migrasi. Jalankan juga sebelum backfill dan simpan hasilnya. Jika tidak, Anda kehilangan bukti untuk membedakan nilai turunan dari nilai yang dibuat-buat.
Indeks harus menjawab pola akses yang diamati
Setujui indeks bila mendukung kueri yang diketahui, menegakkan aturan keunikan yang dinyatakan, atau memenuhi kebutuhan operasional. Mengindeks setiap kolom yang bentuknya seperti identitas membuang penyimpanan dan pekerjaan tulis, sedangkan satu indeks gabungan penting yang hilang dapat mengubah halaman daftar rutin menjadi pemindaian yang terus membesar.
Mulailah dari kueri yang benar-benar dijalankan aplikasi hasil generasi. Catat kolom filter, batas tenant, kolom join, pengurutan, dan ukuran hasil yang diharapkan. Untuk halaman pesanan terbaru, bentuk kueri ini lebih penting daripada diagram tabel:
SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
AND status = $2
ORDER BY created_at DESC
LIMIT 50;
Indeks pada tenant_id saja mungkin tetap memeriksa banyak baris tenant lalu mengurutkannya. Indeks pada (tenant_id, status, created_at DESC) lebih sesuai dengan pola akses ini. Urutan kolom bukan kontes popularitas, melainkan mengikuti kondisi kesetaraan, kondisi rentang, pengurutan, dan selektivitas dalam kueri aktual.
Jalankan EXPLAIN (ANALYZE, BUFFERS) pada data representatif, tetapi jangan anggap satu fixture kecil sebagai bukti performa. PostgreSQL mungkin tepat memilih sequential scan pada tabel kecil. Verifikasi harus memastikan indeks yang dimaksud ada dan latihan berukuran produksi memberi planner pilihan yang realistis. Jangan pernah menonaktifkan sequential scan untuk memaksakan index scan demi persetujuan.
Foreign key juga menciptakan kejutan rutin: PostgreSQL mengindeks kolom primary atau unique yang direferensikan, tetapi tidak otomatis membuat indeks pada kolom anak yang mereferensikan. Karena itu, menghapus atau memperbarui induk dapat memindai tabel anak untuk memeriksa referensi. Join dari anak ke induk juga mungkin membutuhkan indeks anak tersebut. Periksa setiap relasi berdasarkan pembacaan yang diharapkan dan perubahan pada induk.
Tolak indeks duplikat dan tidak terpakai dalam proposal awal. (tenant_id, status) mungkin berlebihan bila sudah ada indeks (tenant_id, status, created_at) yang sesuai, meski rincian beban kerja dapat mengubah penilaian itu. Bandingkan definisi, bukan nama. Builder AI sering membuat satu indeks per fitur dan tidak menyadari beberapa fitur meminta kolom awal yang sama.
Untuk basis data yang sudah sibuk, ingat bahwa CREATE INDEX CONCURRENTLY tidak dapat berjalan di dalam blok transaksi, membutuhkan lebih banyak pekerjaan, dan dapat meninggalkan indeks tidak valid setelah gagal. Manual PostgreSQL menjelaskan perbedaan operasional itu. Framework migrasi yang membungkus setiap migrasi dalam transaksi memerlukan pengecualian dan prosedur pembersihan yang eksplisit, bukan sekadar mengganti kata kunci dengan harapan.
Foreign key memerlukan kepemilikan dan aturan penghapusan
Foreign key benar hanya setelah tim memutuskan apakah relasi itu menyatakan kepemilikan, referensi, konteks opsional, atau atribusi historis. Kolom yang tampak serupa dapat memerlukan perilaku penghapusan yang berlawanan.
Pertimbangkan projects.owner_user_id, invoices.customer_id, dan audit_events.actor_user_id. Proyek dapat memindahkan kepemilikan. Faktur mungkin harus tetap ada setelah akun pelanggan ditutup. Peristiwa audit mungkin perlu menyimpan pengenal lama pelaku meski data identitas sudah dihapus. Menerapkan ON DELETE CASCADE ke ketiganya karena semuanya mereferensikan users akan menyandikan fiksi yang merusak.
Gunakan CASCADE bila anak tidak bermakna tanpa induk dan penghapusan induk benar-benar berarti penghapusan seluruh agregat. Item baris pesanan sering cocok. Catatan pembayaran, dokumen yang diterbitkan, impor, log, dan bukti moderasi sering tidak cocok. Untuk itu, penolakan, pengarsipan, anonimisasi terkontrol, atau referensi nullable yang dipasangkan dengan bidang snapshot yang dipertahankan mungkin lebih sesuai.
SET NULL juga memerlukan peninjauan semantik. Ini mempertahankan baris anak tetapi menghapus relasi langsung. Jika staf kelak perlu menjelaskan akun mana yang membuat laporan, referensi null mungkin tidak cukup. Menyimpan token historis yang tidak mengidentifikasi atau snapshot dapat menjaga akuntabilitas tanpa menyimpan semua data pribadi, tetapi pilihan retensi yang tepat milik kebijakan produk, bukan tebakan AI.
Periksa kardinalitas ke dua arah. Builder dapat memodelkan satu banding satu dengan menempatkan foreign key tanpa constraint unik, lalu diam-diam mengizinkan banyak baris anak. Builder juga dapat menegakkan keunikan ketika riwayat membutuhkan beberapa versi. Tulis kasus fixture untuk induk dengan nol, satu, dan beberapa anak, lalu nyatakan penyisipan mana yang harus lolos.
Constraint yang dapat ditunda memerlukan alasan khusus. Constraint ini dapat membantu ketika transaksi sementara harus melanggar urutan referensi atau memperbarui baris yang saling bergantung, tetapi menjadikan setiap foreign key ditunda secara default memindahkan kesalahan ke commit dan membuat kegagalan lebih sulit dilacak. Pertahankan penegakan langsung kecuali urutan transaksi nyata memerlukan penundaan.
Periksa katalog setelah menerapkan migrasi ke basis data latihan:
SELECT
conname,
contype,
convalidated,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;
Output representatif memiliki bentuk seperti ini:
conname | contype | convalidated | definition
----------------------+---------+--------------+---------------------------------------
orders_pkey | p | t | PRIMARY KEY (id)
orders_customer_fk | f | t | FOREIGN KEY (customer_id) REFERENCES customers(id)
orders_total_check | c | t | CHECK ((total_cents >= 0))
Bandingkan definisi yang dikembalikan dengan aturan kepemilikan yang disetujui. Keberhasilan migrasi saja tidak mengungkap aksi yang hilang, penundaan yang tak terduga, atau constraint yang belum divalidasi.
Perubahan destruktif tersembunyi dalam SQL yang tampak wajar
Tinjau migrasi sebagai transformasi data, karena DDL yang tampak rapi dapat membuang makna tanpa memakai DROP TABLE yang jelas. Cari penghancuran langsung terlebih dahulu, lalu periksa cast, backfill, penulisan ulang, rename, dan penggantian constraint.
Rename dan drop berbeda secara operasional, meski skema akhirnya terlihat sama. Jika surname menjadi family_name, rename mempertahankan data dan dependensi dengan lebih setia. Menghapus kolom lama lalu menambahkan kolom baru menghasilkan diagram yang sama tetapi mengosongkan semua nilai. Migrasi yang dibuat sering menyimpulkan keadaan akhir tanpa memahami kesinambungan.
Perubahan tipe memerlukan contoh konversi dan kasus penolakan. Mengubah pengenal teks menjadi bilangan bulat dapat menghapus nol di depan atau menolak pengenal campuran. Mengurangi presisi numerik dapat membulatkan nilai. Mengonversi timestamp memerlukan asumsi zona waktu yang dinyatakan. Uji ekspresi USING aktual dengan nilai minimum, maksimum, null, malformed, dan nilai historis yang aneh sebelum mengubah kolom.
Anggap operasi ini memerlukan justifikasi tertulis: menghapus tabel atau kolom, mengubah tipe melalui cast yang lossy, mengganti kolom yang terisi, menambahkan CASCADE, menetapkan NOT NULL setelah backfill yang dibuat otomatis, dan membangun ulang keunikan dengan kolom berbeda. Periksa juga SQL mentah yang disematkan dalam fungsi atau callback migrasi yang dibuat. Pencarian teks adalah filter awal, bukan seluruh peninjauan.
Satu pola kegagalan muncul berulang kali. Data yang diketahui memuat kontak dengan perusahaan opsional, tetapi contoh layar hanya menampilkan kontak bisnis. Builder membuat contacts.company_id NOT NULL dan memasukkan perusahaan hasil generasi bernama Unknown untuk baris yang tidak cocok. Migrasi lolos, jumlahnya cocok, dan setiap foreign key tervalidasi. Datanya tetap salah: kontak individu kini tampak milik perusahaan, laporan mengelompokkan orang yang tidak terkait, dan menghapus placeholder dapat berantai ke kontak nyata.
Perbaikannya bukan default lain. Pulihkan keadaan sumber, jadikan relasi nullable, migrasikan hanya kecocokan yang didukung bukti, dan tambahkan assertion bahwa kumpulan yang tidak cocok sama dengan kontak individu yang diketahui. Inilah alasan fixture semantik harus mencatat relasi yang diharapkan, bukan hanya jumlah baris yang diharapkan.
Alat diff skema membantu, tetapi saya menolak menyetujui hanya dari diff. Rekomendasi itu populer karena diff ringkas dan mudah ditinjau. Diff keliru bila menjadi satu-satunya gerbang karena hanya menunjukkan perubahan struktural, bukan asal nilai backfill, batas transaksi, perilaku lock, atau kebenaran setelah migrasi.
Latihan harus membuktikan hasil dan perilaku kegagalan
Jalankan migrasi lengkap pada pemulihan sekali pakai dari dataset yang diketahui, lalu uji hasil yang dimaksud serta jalur yang terputus atau ditolak. Basis data kosong yang baru berguna untuk menemukan kesalahan urutan, tetapi tidak dapat mengungkap konversi lossy, baris historis yang tidak valid, atau validasi lambat.
Gunakan urutan latihan ini sebagai artefak rilis:
- Pulihkan dataset sebelum perubahan ke basis data terisolasi dan catat jumlah baris serta assertion semantik.
- Ambil skema saat ini, terapkan artefak migrasi yang persis sama, dan simpan semua output berikut waktu serta batas transaksinya.
- Jalankan pemeriksaan katalog, assertion pemetaan, pengujian penolakan constraint, dan kueri aplikasi representatif.
- Bandingkan nilai penting dengan ekspektasi yang dicatat, termasuk kategori yang tidak cocok dan null.
- Jalankan metode pemulihan yang didokumentasikan, lalu ulangi assertion sebelum migrasi pada basis data yang dipulihkan.
Ambil dump khusus skema sebelum dan sesudah:
pg_dump --schema-only --no-owner --no-privileges \
--dbname "$DATABASE_URL" > schema.sql
Tinjau tabel, sequence, indeks, constraint, fungsi, trigger, ekstensi, dan hak akses yang relevan bagi aplikasi. Diff model ORM dapat mengabaikan objek basis data yang tidak dimodelkan aplikasi, terutama trigger, indeks ekspresi, indeks parsial, dan fungsi yang dipasang secara manual.
Tambahkan pengujian negatif yang membuktikan constraint menolak keadaan buruk. Transaksi pengujian dapat mencoba penyisipan tidak valid lalu melakukan rollback tanpa memedulikan hasilnya:
BEGIN;
INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);
ROLLBACK;
Output yang diharapkan sebaiknya menyebut constraint yang dilanggar, misalnya:
ERROR: new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL: Failing row contains (..., 0, 2500, ...).
Jangan membandingkan seluruh teks kesalahan di setiap lingkungan karena detailnya dapat berbeda. Nyatakan SQLSTATE atau identitas constraint dalam pengujian otomatis, dan simpan output yang mudah dibaca untuk peninjau.
Ukur lock dan durasi pada dataset yang cukup besar untuk menyerupai deployment yang direncanakan. Operasi yang selesai seketika pada lima puluh baris dapat memblokir penulisan saat memvalidasi jutaan baris. Untuk basis data produksi awal yang kosong, risiko langsung lebih rendah, tetapi latihan tetap menguji data seed yang diimpor dan menciptakan baseline untuk perubahan berikutnya.
Pemulihan membutuhkan lebih dari migrasi terbalik
Pemulihan hanya dapat dipercaya jika mengembalikan data dan kompatibilitas aplikasi dalam waktu yang dapat ditoleransi layanan. Migrasi terbalik yang membuat ulang kolom yang dihapus tidak mengembalikan nilai lamanya.
Tentukan unit pemulihan sebelum eksekusi. Untuk basis data awal yang kosong, menghapus dan membuat ulang basis data mungkin dapat diterima jika belum ada penulisan pengguna. Setelah penulisan nyata ada, pemulihan mungkin memerlukan snapshot basis data, backup logis, kolom lama yang dipertahankan, atau perbaikan maju. Metode yang tepat bergantung pada seberapa banyak data baru dapat masuk selama dan setelah migrasi.
Uji perintah pemulihan dan kredensial sebelum mengandalkannya. Backup yang ada tetapi tidak dapat dipulihkan operator deployment bukan rencana pemulihan. Pulihkan ke basis data terpisah, verifikasi kepemilikan dan ekstensi, lalu jalankan assertion yang diketahui sama seperti sebelum migrasi.
Snapshot dan rollback transaksi menyelesaikan kegagalan yang berbeda. Transaksi dapat membatalkan pernyataan saat migrasi gagal sebelum commit, asalkan setiap operasi ikut dalam transaksi tersebut. Snapshot dapat mengembalikan seluruh basis data ke keadaan sebelumnya, tetapi dapat membuang penulisan sah yang terjadi setelah snapshot. Keduanya tidak otomatis merekonsiliasi penulisan itu.
Pilih perubahan aditif saat ketidakpastian masih ada. Tambahkan kolom atau tabel baru, salin data dengan aturan yang dapat diukur, jalankan kedua jalur kode selama periode terkontrol jika perlu, lalu hapus struktur lama hanya setelah verifikasi. Pendekatan expand and contract membutuhkan pekerjaan tambahan, tetapi menjaga bukti. Menyimpan kolom lama yang sudah di-rename selama satu rilis sering lebih murah daripada membangunnya kembali dari log.
Tulis pemicu pemulihan lebih dahulu. Contohnya mencakup assertion semantik yang gagal, catatan tidak cocok yang tak terduga, constraint tidak valid, migrasi yang melampaui jendela lock yang disetujui, atau kesalahan aplikasi akibat ketidakcocokan versi. Operator tidak seharusnya menciptakan keputusan ketika pengguna sedang menunggu.
Catat titik ketika memulihkan basis data lama juga memerlukan pemulihan aplikasi lama. Aplikasi baru dapat bergantung pada kolom baru, sementara aplikasi lama dapat menolak nilai enum baru atau menulis bentuk lama. Pemulihan basis data dan aplikasi harus memakai versi yang kompatibel.
Persetujuan membutuhkan bukti, bukan keyakinan
Setujui migrasi pertama hanya jika orang lain dapat mengulangi alasan keamanannya dari artefak yang disimpan. Keyakinan dari code review yang bersih atau antarmuka hasil generasi yang rapi tidak bertahan saat muncul ketidaksesuaian data pertama yang tak dapat dijelaskan.
Catatan persetujuan harus memuat asumsi hasil inferensi, matriks pemetaan tabel, identitas dataset yang diketahui, diff skema, migrasi persisnya, kueri dan hasil validasi, pengujian input yang ditolak, alasan indeks, justifikasi operasi destruktif, dan prosedur pemulihan yang telah diuji. Sebutkan peninjau dan pertahankan keputusan yang belum selesai sebagai blocker, jangan menguburnya dalam riwayat chat.
Saat aplikasi dibuat di Koder.ai, gunakan mode perencanaan untuk menuliskan keputusan skema ini sebelum mengizinkan migrasi, ekspor sumber untuk ditinjau, dan perlakukan snapshot serta rollback sebagai alat pemulihan yang tetap perlu dilatih terhadap dataset yang diketahui.
Jangan biarkan builder menyetujui inferensinya sendiri dengan membuat ulang kode sampai pengujian lulus. Siklus itu dapat membuat aplikasi menyesuaikan diri dengan skema yang keliru, bukan memperbaiki model. Manusia harus memutuskan apakah basis data sesuai dengan domain, terutama terkait identitas, penghapusan, retensi, dan nilai yang tidak diketahui.
Kueri persetujuan terakhir harus membosankan. Setiap fakta yang diketahui dipetakan ke satu hasil yang diharapkan, setiap constraint menolak contoh tandingan yang dimaksud, setiap operasi destruktif memiliki alasan, dan pemulihan menghasilkan kembali assertion sebelum migrasi. Jika bukti memerlukan penjelasan yang meyakinkan untuk memaafkan ketidakcocokan, hentikan migrasi. PostgreSQL akan menegakkan skema secara tepat, termasuk bagian yang ditebak salah oleh builder.
Pertanyaan umum
Apa yang harus saya periksa dalam skema PostgreSQL yang dibuat AI?
Periksa DDL yang dibuat, operasi migrasi, dan asumsi di balik keduanya. Skema akhir yang benar tetap dapat dicapai lewat migrasi yang menghapus data, memblokir penulisan, atau menciptakan nilai default yang menyesatkan.
Seberapa besar dataset untuk verifikasi skema?
Gunakan dataset kecil yang berisi baris biasa, nilai batas, relasi yang hilang, duplikat, null, string kosong, dan keanehan historis. Tujuannya bukan volume, melainkan membantah asumsi builder sebelum data produksi yang melakukannya.
Apakah migrasi pengujian yang berhasil membuktikan skema aman?
Tidak. Migrasi yang berhasil membuktikan PostgreSQL menerima pernyataan tersebut pada keadaan basis data itu. Hal itu tidak membuktikan pemetaan tabel benar, data mempertahankan maknanya, indeks mendukung kueri nyata, atau pemulihan berfungsi.
Kapan kolom PostgreSQL harus NOT NULL?
Kolom sebaiknya NOT NULL hanya jika setiap catatan yang sah memiliki nilai dan aplikasi dapat menyediakannya dalam setiap jalur penulisan. Jangan memakai default buatan hanya untuk memenuhi constraint, karena itu mengganti data yang jelas-jelas hilang dengan data palsu yang tampak meyakinkan.
Haruskah saya memakai constraint unik atau indeks unik?
Constraint unik menyatakan aturan yang dapat dirujuk objek basis data lain, dan PostgreSQL mendukungnya dengan indeks. Indeks unik berguna saat keunikan hanya berlaku pada baris atau ekspresi tertentu, misalnya catatan yang belum dihapus atau alamat email yang sudah dinormalisasi.
Apakah foreign key PostgreSQL otomatis membuat indeks?
Indeksilah kolom yang dipakai untuk menemukan baris induk, memfilter kueri umum, menggabungkan tabel besar, atau menegakkan keunikan. PostgreSQL tidak otomatis mengindeks sisi referensi dari foreign key, jadi periksa kolom anak secara terpisah dan jangan menganggap constraint sudah menanganinya.
Kapan ON DELETE CASCADE aman?
Gunakan CASCADE hanya jika baris anak tidak lagi memiliki makna mandiri setelah induknya hilang. Jika penghapusan adalah keputusan bisnis atau anak merupakan bukti seperti faktur atau catatan audit, tolak atau proses penghapusan itu secara eksplisit.
Bagaimana mendeteksi perubahan destruktif dalam migrasi?
Anggap setiap DROP, cast yang mempersempit, penulisan ulang tabel, kolom wajib baru, dan constraint pengganti berpotensi merusak. Cari dalam teks migrasi, lalu periksa juga fungsi yang dibuat dan SQL mentah karena perilaku merusak dapat tersembunyi di dalamnya.
Apa cara paling aman untuk menguji pemulihan migrasi?
Pulihkan basis data sebelum perubahan ke lokasi terpisah, jalankan migrasi di sana, lakukan kueri verifikasi semantik, lalu bandingkan hasilnya dengan ekspektasi yang dicatat. Menguji migrasi terbalik saja tidak menemukan data yang hilang dan dapat memberi rasa aman palsu.
Bukti apa yang harus saya simpan setelah menyetujui skema?
Simpan DDL yang dibuat, teks migrasi, diff skema, kueri dan hasil verifikasi, prosedur pemulihan, serta identitas peninjau di satu tempat. Catatan itu menjelaskan apa yang disetujui dan membantu peninjauan migrasi berikutnya menemukan asumsi yang telah berubah.