Mode perencanaan skema Postgres membantu Anda menentukan entitas, constraint, indeks, dan migrasi sebelum menghasilkan kode, sehingga mengurangi penulisan ulang nantinya.

Jika Anda membuat endpoint dan model sebelum bentuk database jelas, biasanya Anda akan menulis ulang fitur yang sama dua kali. Aplikasi bekerja untuk demo, lalu data nyata dan kasus tepi muncul dan semuanya mulai terasa rapuh.
Sebagian besar penulisan ulang muncul dari tiga masalah yang bisa diprediksi:
Masing-masing memaksa perubahan yang berimbas ke kode, test, dan aplikasi klien.
Merencanakan skema Postgres berarti menetapkan kontrak data dulu, lalu menghasilkan kode yang sesuai. Dalam praktiknya, itu terlihat seperti menuliskan entitas, relasi, dan beberapa kueri penting, lalu memilih constraint, indeks, dan pendekatan migrasi sebelum alat apa pun membuat tabel dan CRUD.
Hal ini lebih penting lagi ketika Anda menggunakan platform vibe-coding seperti Koder.ai, yang bisa menghasilkan banyak kode dengan cepat. Generasi cepat bagus, tapi jauh lebih andal ketika skema sudah matang. Model dan endpoint yang digenerasi perlu lebih sedikit suntingan nantinya.
Berikut yang biasanya salah ketika Anda melewatkan perencanaan:
Rencana skema yang baik sederhana: deskripsi entitas dalam bahasa biasa, draft tabel dan kolom, constraint dan indeks kunci, serta strategi migrasi yang memungkinkan Anda mengubah dengan aman saat produk tumbuh.
Perencanaan skema bekerja paling baik ketika Anda mulai dari apa yang aplikasi harus ingat dan apa yang orang harus bisa lakukan dengan data itu. Tulis tujuan dalam 2–3 kalimat sederhana. Jika Anda tidak bisa menjelaskannya dengan sederhana, Anda mungkin membuat tabel ekstra yang tidak perlu.
Selanjutnya, fokus pada aksi yang membuat atau mengubah data. Aksi-aksi ini adalah sumber baris Anda dan mengungkap apa yang harus divalidasi. Pikirkan dalam kata kerja, bukan kata benda.
Misalnya, aplikasi booking mungkin perlu membuat booking, menjadwalkan ulang, membatalkan, mengembalikan dana, dan mengirim pesan ke pelanggan. Kata kerja itu cepat menunjukkan apa yang harus disimpan (slot waktu, perubahan status, jumlah uang) sebelum Anda menamai tabel.
Tangkap juga jalur baca Anda, karena pembacaan memengaruhi struktur dan indeks nantinya. Daftar layar atau laporan yang benar-benar dipakai orang dan bagaimana mereka menyaring data: “My bookings” diurutkan berdasarkan tanggal dan difilter berdasarkan status, pencarian admin berdasarkan nama pelanggan atau referensi booking, pendapatan harian per lokasi, dan tampilan audit siapa mengubah apa dan kapan.
Terakhir, catat kebutuhan non-fungsional yang mengubah pilihan skema, seperti riwayat audit, soft delete, pemisahan multi-tenant, atau aturan privasi (mis. membatasi siapa yang bisa melihat detail kontak).
Jika Anda berencana menghasilkan kode setelah ini, catatan ini menjadi prompt yang kuat. Mereka menjelaskan apa yang diperlukan, apa yang bisa berubah, dan apa yang harus bisa dicari. Jika Anda menggunakan Koder.ai, menuliskannya sebelum menghasilkan apapun membuat Planning Mode jauh lebih efektif karena platform bekerja dari kebutuhan nyata, bukan tebakan.
Sebelum menyentuh tabel, tulis deskripsi sederhana tentang apa yang aplikasi Anda simpan. Mulai dengan mendaftar kata benda yang sering Anda ulang: user, project, message, invoice, subscription, file, comment. Setiap kata adalah kandidat entitas.
Kemudian tambahkan satu kalimat per entitas yang menjawab: apa itu, dan mengapa ada? Misalnya: “A Project is a workspace a user creates to group work and invite others.” Ini mencegah tabel samar seperti data, items, atau misc.
Kepemilikan adalah keputusan besar berikutnya, dan memengaruhi hampir setiap kueri yang Anda tulis. Untuk tiap entitas, putuskan:
Sekarang putuskan bagaimana Anda akan mengidentifikasi record. UUID bagus ketika record bisa dibuat dari banyak tempat (web, mobile, background jobs) atau ketika Anda tidak menginginkan ID yang dapat ditebak. Bigint lebih kecil dan lebih cepat. Jika Anda perlu identifier yang ramah manusia, simpan terpisah (mis. project_code pendek yang unik dalam sebuah akun) daripada menjadikannya primary key.
Terakhir, tulis relasi dalam kata-kata sebelum Anda membuat diagram: seorang user memiliki banyak project, sebuah project memiliki banyak message, dan user bisa bergabung di banyak project. Tandai setiap tautan sebagai wajib atau opsional, seperti “a message must belong to a project” vs “an invoice may belong to a project.” Kalimat-kalimat ini menjadi sumber kebenaran untuk generasi kode nanti.
Setelah entitas jelas dalam bahasa biasa, ubah masing-masing menjadi tabel dengan kolom yang mencerminkan fakta nyata yang perlu disimpan.
Mulai dengan nama dan tipe yang bisa Anda pertahankan. Pilih pola konsisten: nama kolom snake_case, tipe yang sama untuk ide yang sama, dan primary key yang dapat diprediksi. Untuk timestamp, pilih timestamptz supaya zona waktu tidak mengejutkan Anda nanti. Untuk uang, gunakan numeric(12,2) (atau simpan sen sebagai integer) daripada float.
Untuk field status, gunakan enum Postgres atau kolom text dengan CHECK constraint sehingga nilai yang diizinkan terkontrol.
Tentukan apa yang wajib vs opsional dengan menerjemahkan aturan ke NOT NULL. Jika sebuah nilai harus ada agar baris masuk akal, jadikan required. Jika benar-benar tidak diketahui atau tidak berlaku, izinkan null.
Satu set kolom default yang praktis untuk direncanakan:
id (uuid atau bigint, pilih satu pendekatan dan konsisten)created_at dan updated_atdeleted_at hanya jika Anda benar-benar membutuhkan soft deletes dan restorecreated_by ketika Anda perlu jejak audit siapa yang melakukan apaRelasi many-to-many hampir selalu menjadi tabel join. Misalnya, jika banyak user bisa berkolaborasi pada sebuah app, buat app_members dengan app_id dan user_id, lalu tegakkan keunikan pada pasangan tersebut agar duplikat tidak terjadi.
Pikirkan tentang riwayat lebih awal. Jika Anda tahu akan membutuhkan versioning, rencanakan tabel immutable seperti app_snapshots, di mana tiap baris adalah versi yang disimpan yang terhubung ke apps lewat app_id dan diberi cap waktu lewat created_at.
Constraint adalah pagar pengaman skema Anda. Tentukan aturan mana yang harus selalu benar tidak peduli layanan, skrip, atau alat admin mana yang mengakses database.
Mulai dengan identitas dan relasi. Setiap tabel butuh primary key, dan setiap kolom “belongs to” harus menjadi foreign key nyata, bukan sekadar integer yang Anda harap cocok.
Kemudian tambahkan uniqueness di mana duplikasi akan menyebabkan kerusakan nyata, seperti dua akun dengan email yang sama atau dua line item dengan (order_id, product_id) yang sama.
Constraint bernilai tinggi untuk direncanakan sejak awal:
amount >= 0, status IN ('draft','paid','canceled'), atau rating BETWEEN 1 AND 5.Perilaku cascade adalah tempat perencanaan menyelamatkan Anda nanti. Tanyakan apa yang sebenarnya diharapkan orang. Jika seorang customer dihapus, biasanya order mereka tidak boleh hilang. Itu menunjuk ke restrict deletes dan menjaga riwayat. Untuk data tergantung seperti order line items, cascading dari order ke items masuk akal karena item tidak punya makna tanpa parent.
Ketika Anda nanti menghasilkan model dan endpoint, constraint ini menjadi persyaratan yang jelas: error apa yang ditangani, field apa yang wajib, dan kasus tepi apa yang tidak mungkin terjadi karena desain.
Indeks seharusnya menjawab satu pertanyaan: apa yang perlu cepat untuk pengguna nyata.
Mulai dari layar dan panggilan API yang Anda harapkan akan dikirim pertama. Halaman daftar yang memfilter berdasarkan status dan mengurutkan berdasarkan terbaru punya kebutuhan berbeda dibanding halaman detail yang memuat record terkait.
Tulis 5 sampai 10 pola kueri dalam bahasa biasa sebelum memilih indeks. Misalnya: “Tampilkan invoice saya 30 hari terakhir, filter by paid/unpaid, sort by created_at,” atau “Buka project dan daftar tugasnya berdasarkan due_date.” Ini menjaga pilihan indeks berakar pada penggunaan nyata.
Set indeks awal yang baik sering termasuk kolom foreign key yang dipakai untuk join, kolom filter umum (seperti status, user_id, created_at), dan satu atau dua indeks komposit untuk kueri multi-filter yang stabil, seperti (account_id, created_at) ketika Anda selalu memfilter berdasarkan account_id lalu mengurutkan berdasarkan waktu.
Urutan kolom di indeks komposit penting. Letakkan kolom yang paling sering Anda filter (dan paling selektif) di depan. Jika Anda selalu memfilter dengan tenant_id, biasanya kolom itu ditempatkan di depan banyak indeks.
Hindari mengindeks semuanya “untuk jaga-jaga.” Setiap indeks menambah kerja pada INSERT dan UPDATE, dan itu bisa merugikan lebih dari sedikit perlambatan kueri yang jarang terjadi.
Rencanakan pencarian teks terpisah. Jika Anda hanya butuh pencocokan “contains” sederhana, ILIKE mungkin cukup di awal. Jika pencarian adalah hal inti, rencanakan full-text search (tsvector) sejak awal agar Anda tidak perlu merancang ulang nanti.
Skema tidak “selesai” ketika Anda membuat tabel pertama. Itu berubah setiap kali Anda menambah fitur, memperbaiki kesalahan, atau mempelajari lebih banyak tentang data Anda. Jika Anda memutuskan strategi migrasi sejak dini, Anda menghindari penulisan ulang yang menyakitkan setelah kode digenerasi.
Pegang aturan sederhana: ubah database dalam langkah kecil, satu fitur pada satu waktu. Tiap migrasi harus mudah direview dan aman dijalankan di setiap lingkungan.
Sebagian besar kerusakan muncul dari mengganti nama atau menghapus kolom, atau mengubah tipe. Alih-alih melakukan semuanya sekaligus, rencanakan jalur aman:
Ini membutuhkan lebih banyak langkah, tapi memang lebih cepat dalam kenyataan karena mengurangi outage dan patch darurat.
Seed data juga bagian dari migrasi. Putuskan tabel referensi mana yang “selalu ada” (roles, statuses, countries, plan types) dan buat mereka dapat diprediksi. Masukkan insert dan update untuk tabel-tabel ini ke migrasi khusus sehingga setiap developer dan setiap deploy mendapat hasil yang sama.
Tetapkan ekspektasi sejak awal:
Rollback tidak selalu sempurna sebagai “down migration.” Kadang cara terbaik untuk rollback adalah restore dari backup. Jika Anda menggunakan Koder.ai, juga layak memutuskan kapan mengandalkan snapshot dan rollback untuk pemulihan cepat, terutama sebelum perubahan berisiko.
Bayangkan sebuah SaaS kecil di mana orang bergabung ke tim, membuat proyek, dan melacak tugas.
Mulailah dengan mendaftar entitas dan hanya field yang Anda butuhkan di hari pertama:
Relasi sederhana: sebuah team punya banyak projects, sebuah project punya banyak tasks, dan user bergabung ke team lewat team_members. Tasks milik sebuah project dan bisa ditugaskan ke user.
Sekarang tambahkan beberapa constraint untuk mencegah bug yang biasanya ditemukan terlambat:
Indeks harus sesuai layar nyata. Misalnya, jika daftar tugas memfilter berdasarkan project dan state lalu mengurutkan berdasarkan terbaru, rencanakan indeks seperti tasks (project_id, state, created_at DESC). Jika “My tasks” adalah tampilan penting, indeks seperti tasks (assignee_user_id, state, due_date) dapat membantu.
Untuk migrasi, buat set pertama yang aman dan sederhana: create tables, primary keys, foreign keys, dan constraint unik inti. Perubahan tindak lanjut yang baik adalah sesuatu yang Anda tambahkan setelah penggunaan membuktikan itu diperlukan, mis. memperkenalkan soft delete (deleted_at) pada tasks dan menyesuaikan indeks “active tasks” agar mengabaikan baris yang dihapus.
Sebagian besar penulisan ulang terjadi karena skema pertama kekurangan aturan dan detail penggunaan nyata. Putaran perencanaan yang baik bukan tentang diagram sempurna. Ini tentang melihat jebakan lebih awal.
Kesalahan umum adalah menaruh aturan penting hanya di kode aplikasi. Jika sebuah nilai harus unik, hadir, atau berada dalam rentang, database harus menegakkannya juga. Kalau tidak, background job, endpoint baru, atau import manual bisa melewati logika Anda.
Kesalahan lain sering meremehkan indeks sebagai masalah nanti. Menambahkannya setelah peluncuran sering berubah jadi tebak-tebakan, dan Anda bisa mengindeks hal yang salah sementara kueri lambat sebenarnya adalah join atau filter pada kolom status.
Tabel many-to-many juga sumber bug yang sunyi. Jika tabel join Anda tidak mencegah duplikat, Anda bisa menyimpan relasi yang sama dua kali dan menghabiskan waktu berjam-jam men-debug “mengapa user ini punya dua role?”
Mudah juga membuat tabel dulu lalu sadar bahwa Anda butuh audit logs, soft deletes, atau event history. Penambahan itu berimbas ke endpoint dan laporan.
Terakhir, kolom JSON menggoda untuk data “fleksibel”, tapi menghapus pengecekan dan membuat indexing lebih sulit. JSON baik untuk payload yang benar-benar variabel, bukan field bisnis inti.
Sebelum Anda menghasilkan kode, jalankan daftar perbaikan cepat ini:
Berhentilah sejenak dan pastikan rencana cukup lengkap untuk menghasilkan kode tanpa kejar-kejaran kejutan. Tujuannya bukan kesempurnaan. Tujuannya menangkap celah yang menyebabkan penulisan ulang nanti: relasi yang hilang, aturan yang tidak jelas, dan indeks yang tidak cocok dengan cara aplikasi dipakai.
Gunakan ini sebagai pemeriksaan pra-penerbangan cepat:
amount >= 0 atau status yang diizinkan).Tes kewarasan cepat: bayangkan seorang rekan bergabung besok. Bisakah mereka membangun endpoint pertama tanpa bertanya “bolehkah ini null?” atau “apa yang terjadi saat dihapus?” setiap jam?
Setelah rencana terbaca jelas dan alur utama masuk akal di atas kertas, ubah menjadi sesuatu yang bisa dieksekusi: skema nyata plus migrasi.
Mulailah dengan migrasi awal yang membuat tabel, tipe (jika Anda menggunakan enum), dan constraint yang wajib. Buat langkah pertama kecil tapi benar. Muat sedikit seed data dan jalankan kueri yang benar-benar dibutuhkan aplikasi. Jika suatu alur terasa canggung, perbaiki skema sementara riwayat migrasi masih pendek.
Hasilkan model dan endpoint hanya setelah Anda bisa menguji beberapa aksi end-to-end dengan skema di tempatnya (create, update, list, delete, plus satu aksi bisnis nyata). Generasi kode berjalan paling cepat ketika tabel, kunci, dan penamaan cukup stabil sehingga Anda tidak mengganti nama semuanya keesokan harinya.
Loop praktis yang menjaga penulisan ulang rendah:
Putuskan sejak awal apa yang Anda validasi di database vs di layer API. Taruh aturan permanen di database (foreign keys, unique constraints, check constraints). Simpan aturan lunak di API (feature flags, batas sementara, dan logika lintas-tabel yang sering berubah).
Jika Anda menggunakan Koder.ai, pendekatan yang masuk akal adalah menyetujui entitas dan migrasi di Planning Mode dulu, lalu hasilkan backend Go plus PostgreSQL Anda. Ketika perubahan melenceng, snapshot dan rollback dapat membantu Anda kembali ke versi yang diketahui baik dengan cepat sambil menyesuaikan rencana skema.
Rencanakan skema terlebih dahulu. Itu menetapkan kontrak data yang stabil (tabel, kunci, constraint) sehingga model dan endpoint yang digenerasi tidak perlu sering diganti nama atau ditulis ulang nanti.
Dalam praktiknya: tuliskan entitas Anda, relasi, dan kueri teratas, lalu kunci constraint, indeks, dan migrasi sebelum Anda menghasilkan kode.
Tuliskan 2–3 kalimat yang menjelaskan apa yang aplikasi harus ingat dan apa yang pengguna harus bisa lakukan.
Lalu daftar:
Ini memberi Anda cukup kejelasan untuk mendesain tabel tanpa berlebihan.
Mulailah dengan mencatat kata benda yang sering muncul (user, project, invoice, task). Untuk tiap entitas, tambahkan satu kalimat: apa itu dan mengapa ada.
Jika Anda tidak bisa mendeskripsikannya dengan jelas, besar kemungkinan Anda akan membuat tabel samar seperti items atau misc dan menyesalinya nanti.
Gunakan satu strategi ID konsisten di seluruh skema.
UUID: cocok ketika record bisa dibuat dari banyak tempat (web/mobile/job) atau Anda tidak ingin ID yang mudah ditebakbigint: lebih kecil, sedikit lebih cepat, dan sederhana jika semuanya dibuat dari serverJika perlu identifier yang ramah manusia, tambahkan kolom unik terpisah (mis. project_code) daripada menjadikannya primary key.
Tentukan untuk tiap relasi berdasarkan ekspektasi pengguna dan apa yang harus disimpan.
Default umum:
RESTRICT/NO ACTION ketika menghapus parent akan menghapus catatan penting (mis. customer → orders)CASCADE ketika child tidak punya arti tanpa parent (mis. order → line items)Putuskan ini lebih awal karena memengaruhi perilaku API dan kasus tepi.
Letakkan aturan permanen di database sehingga setiap penulis (API, script, import, admin) dipaksa patuh.
Prioritaskan:
Mulai dari pola kueri nyata, bukan dugaan.
Tulis 5–10 kueri dalam bahasa sehari-hari (filter + sort), lalu buat indeks untuk itu:
status, user_id, created_atBuat tabel join dengan dua foreign key dan unique komposit.
Contoh pola:
team_members(team_id, user_id, role, joined_at)UNIQUE (team_id, user_id) untuk mencegah duplikasiIni mencegah bug halus seperti “mengapa user ini muncul dua kali?” dan membuat kueri lebih bersih.
Default yang baik:
timestamptz untuk timestamp (lebih sedikit masalah zona waktu)numeric(12,2) atau integer cents untuk uang (hindari float)CHECK constraintJaga konsistensi tipe antar tabel sehingga join dan validasi tetap dapat diprediksi.
Gunakan migrasi kecil yang mudah direview dan hindari perubahan besar sekaligus.
Jalur aman:
Juga tentukan sejak awal bagaimana menangani seed/reference data sehingga setiap lingkungan konsisten.
PRIMARY KEY pada setiap tabelFOREIGN KEY untuk setiap kolom “belongs to”UNIQUE ketika duplikasi berbahaya (email, (team_id, user_id) di tabel join)CHECK untuk aturan sederhana (jumlah non-negatif, status yang diizinkan)NOT NULL untuk field yang wajib ada agar baris masuk akal(account_id, created_at))Hindari mengindeks semuanya; setiap indeks memperlambat INSERT dan UPDATE.