Hal yang Saya Harap Pernah Diberitahukan Seseorang tentang Postgres
(challahscript.com)- Dokumentasi resmi Postgres sangat bagus, tetapi PDF Postgres 17 mencapai 3.200 halaman, sehingga sulit bagi pemula untuk mempelajari desain skema, perilaku SQL, dan jebakan operasional hanya dari dokumentasi sebelum praktik
- Jika tidak ada alasan khusus, data sebaiknya dinormalisasi; denormalisasi dengan menyimpan data duplikat demi performa baca harus dibayar dengan risiko inkonsistensi dan kompleksitas penulisan
- Kata kunci SQL tidak peka huruf besar-kecil, tetapi NULL lebih dekat dengan “tidak diketahui”, sehingga membandingkannya seperti
nulldi bahasa pemrograman umum dapat menghasilkan hasil yang tidak terduga - Dengan memanfaatkan pager,
\x,.psqlrc,\pset null, autocomplete, perintah backslash, dan\copydipsql, keterbacaan output, eksplorasi, dan ekspor CSV menjadi jauh lebih mudah - Indeks, lock, transaksi, dan JSONB memang kuat, tetapi jika tidak memahami query plan dan batasan operasional, hal itu dapat berujung pada penurunan performa atau masalah ketersediaan
Konteks yang perlu diketahui sebelum menyelami dokumentasi resmi yang sangat besar
- Dokumentasi resmi Postgres, untuk versi 17 saat ini, jika dicetak sebagai PDF ukuran US letter, berjumlah 3.200 halaman; jika dicetak ukuran A4, jumlahnya 3.024 halaman
- Ada banyak pengetahuan praktis yang berguna untuk diketahui sebelum memakai Postgres, dan sebagian dapat berlaku juga untuk DBMS SQL lain, tetapi cakupan penerapannya tidak selalu pasti
Secara default, normalisasikan data
- Normalisasi adalah proses menghapus data yang duplikatif atau tidak perlu dari skema database
- Jika tabel
documentsmenyimpanuser_emailsecara langsung, ketika pengguna mengganti email, semua baris dokumen milik pengguna tersebut harus diperbarui- Sebagai gantinya, setiap baris di
documentsdapat dibuat agar merujuk ke baris di tabel lain sepertiusersmelalui foreign keyuser_id
- Sebagai gantinya, setiap baris di
- Tidak perlu menghafal semua bentuk normalisasi seperti “1st normal form”, tetapi proses normalisasi umum dapat menghasilkan skema yang lebih mudah dipelihara
- Denormalisasi adalah cara menyimpan data duplikat agar data tertentu dapat dibaca cepat tanpa dihitung ulang setiap kali
- Dalam aplikasi shift kerja karyawan, total jam kerja tahun ini dapat dihitung dan disimpan secara berkala atau saat jam kerja berubah, alih-alih menghitungnya setiap kali dari penjumlahan semua durasi shift
- Data ini bisa disimpan di dalam Postgres atau di lapisan cache seperti Redis
- Denormalisasi hampir selalu memiliki biaya, dan biaya utamanya adalah kemungkinan inkonsistensi data serta meningkatnya kompleksitas penulisan
Saran “jangan lakukan ini” dari proyek Postgres
- Wiki resmi Postgres memiliki daftar “Don’t do this”
- Tidak apa-apa jika tidak memahami semua item; item yang tidak dipahami kemungkinan juga bukan kesalahan yang mudah Anda lakukan
- Beberapa saran berikut khususnya layak diingat
- Gunakan tipe
textuntuk menyimpan teks - Gunakan
timestampz/time with time zoneuntuk menyimpan timestamp - Beri nama tabel dengan snake_case
- Gunakan tipe
Perilaku SQL yang mudah membingungkan
-
Kata kunci SQL tidak harus ditulis dengan huruf besar
- Kata kunci SQL tidak membedakan huruf besar-kecil
- Query berikut memiliki makna yang sama
SELECT * FROM my_table WHERE x = 1 AND y > 2 LIMIT 10; select * from my_table where x = 1 and y > 2 limit 10; SELECT * from my_table WHERE x = 1 and y > 2 LIMIT 10;- Karakteristik ini tidak terbatas pada Postgres saja
-
NULL berbeda dari null/nil di bahasa pemrograman umum
NULLdi SQL lebih dekat dengan “tidak diketahui” daripadanullataunildi bahasa pemrograman umumNULL = NULLtidak mengembalikantrue, melainkanNULL- Sebagian besar perbandingan yang salah satu sisinya
NULLjuga menghasilkanNULL - Untuk membandingkan
NULL, gunakan operasi berikutx IS NULL:truejikaxadalahNULLx IS NOT NULL:truejikaxbukanNULLx IS NOT DISTINCT FROM y: miripx = y, tetapi memperlakukanNULLseperti nilai biasax IS DISTINCT FROM y: miripx != y/x <> y, tetapi memperlakukanNULLseperti nilai biasa
- Klausa
WHEREhanya mengembalikan baris ketika kondisinyatrueSELECT * FROM users WHERE title != 'manager'tidak mengembalikan baris yangtitle-nyaNULL- Karena hasil dari
NULL != 'manager'adalahNULL
COALESCEmengembalikan nilai pertama yang bukanNULLdari beberapa argumen
COALESCE(NULL, 5, 10) = 5 COALESCE(2, NULL, 9) = 2 COALESCE(NULL, NULL) IS NULL
Menggunakan psql dengan lebih berguna
-
Meningkatkan keterbacaan output
- Jika output sulit dibaca saat menampilkan tabel dengan banyak kolom atau nilai panjang, pager mungkin sedang dimatikan
- Pager terminal memungkinkan teks besar atau tabel
psqldigulir dalam viewport - Untuk tabel dengan banyak kolom, expanded mode dapat diaktifkan dengan
\pset expandedatau\x - Jika ingin menjadikannya default, tambahkan
\xke~/.psqlrcdi direktori home
-
Memperjelas output NULL
- Pengaturan default tidak menampilkan dengan jelas apakah sebuah nilai adalah
NULL - Di
psql, Anda dapat menentukan string tampilan untukNULL
\pset null '[NULL]'- String Unicode juga bisa digunakan; untuk menjadikannya default, tambahkan perintah yang sama ke
~/.psqlrc
- Pengaturan default tidak menampilkan dengan jelas apakah sebuah nilai adalah
-
Memanfaatkan autocomplete dan perintah backslash
psqlmendukung autocomplete seperti konsol interaktif- Setelah mengetik sebagian kata kunci atau nama tabel, tekan Tab untuk melengkapinya
- Perintah backslash yang berguna antara lain
\?: daftar semua shortcut\d: menampilkan relation, yaitu daftar tabel dan sequence beserta pemiliknya\d+: menambahkan ukuran dan sebagian metadata ke\d\d table_name: menampilkan skema tabel, tipe kolom, apakah nullable, nilai default, indeks, dan constraint foreign key\e: mengedit query di editor default yang ditentukan oleh variabel lingkungan$EDITOR\h SQL_KEYWORD: menampilkan sintaks kata kunci SQL tersebut dan tautan dokumentasi
-
Ekspor CSV dan alias SELECT
- Dengan
\copy, hasil query dapat disimpan sebagai CSV
\copy (select * from some_table) to 'my_file.csv' CSV- Untuk menyertakan nama kolom di baris pertama, tambahkan opsi
HEADER
\copy (select * from some_table) to 'my_file.csv' CSV HEADER\copydapat menghindari kebutuhan hak akses yang lebih tinggi yang diperlukan oleh statementCOPYyang lebih standar- Kolom output
SELECTdapat diberi alias denganAS
SELECT vendor, COUNT(*) AS number_of_backpacks FROM backpacks GROUP BY vendor ORDER BY number_of_backpacks DESC;- Di
GROUP BYdanORDER BY, Anda dapat merujuk nomor kolom yang muncul setelahSELECT
SELECT vendor, COUNT(*) AS number_of_backpacks FROM backpacks GROUP BY 1 ORDER BY 2 DESC;- Bentuk singkat ini berguna, tetapi sebaiknya tidak dimasukkan ke query yang dideploy ke production
- Dengan
Menambahkan indeks tidak selalu berarti indeks itu akan dipakai
-
Indeks dan query plan
- Indeks adalah struktur data yang berperan sebagai direktori pintasan untuk menemukan baris tabel berdasarkan field tertentu
- Indeks yang paling umum adalah B-tree, dan bekerja untuk kondisi kesetaraan persis seperti
WHERE a = 3serta kondisi rentang sepertiWHERE a > 5 - Anda tidak bisa langsung memerintahkan Postgres untuk memakai indeks tertentu
- Berdasarkan statistik yang dipelihara untuk setiap tabel, Postgres memperkirakan apakah indeks akan lebih cepat daripada sequential scan yang membaca tabel dari awal hingga akhir
- Dengan menambahkan
EXPLAINdi depanSELECT ... FROM ..., Anda dapat melihat query plan tentang bagaimana Postgres akan menjalankan query - Saat membaca query plan, Anda dapat merujuk ke panduan EXPLAIN ANALYZE dari thoughtbot, dokumentasi pganalyze, dokumentasi resmi, dan explain.depesz.com
-
Tabel kecil dan indeks multi-kolom
- Pada tabel dengan sedikit baris, seperti DB pengembangan lokal, indeks mungkin tidak banyak membantu
- Jika hanya sekitar 100 baris, Postgres dapat menilai sequential scan lebih cepat daripada indeks
- Postgres mendukung indeks multi-kolom
CREATE INDEX CONCURRENTLY ON tbl (a, b);- Kondisi seperti
WHERE a = 1 AND b = 2bisa lebih cepat dibandingkan jikaadanbmasing-masing memiliki indeks terpisah - Alasannya, kondisi pencarian dapat digabungkan secara efisien sambil menelusuri satu B-tree
- Indeks
(a, b)juga membuat query yang hanya memfilterasecepat indeks khususa - Query seperti
WHERE b = 5mungkin menjadi lebih cepat, tetapi belum tentu optimal- Karena kunci indeks diurutkan berdasarkan
aterlebih dahulu, lalub, sehingga harus melewati semua nilaiauntuk mencari nilaib
- Karena kunci indeks diurutkan berdasarkan
- Jika perlu melakukan query dengan beberapa kombinasi kolom, sering kali orang memakai indeks
(a, b)bersama indeks khususb - Tergantung kebutuhan, Anda juga bisa mengandalkan indeks khusus masing-masing untuk
adanb
-
Gunakan text_pattern_ops untuk prefix match
- Dengan pendekatan materialized path untuk menyimpan direktori hierarkis, Anda mungkin perlu menemukan semua descendant yang dimulai dengan prefix tertentu
SELECT * FROM directories WHERE path LIKE '/1/2/3/%'- Meski membuat indeks B-tree default pada kolom
path, indeks itu mungkin tidak digunakan untuk query ini
CREATE INDEX CONCURRENTLY ON directories (path);- Untuk memungkinkan pengurutan per karakter yang diperlukan pada prefix match atau pattern match, Anda harus menentukan operator class
CREATE INDEX CONCURRENTLY ON directories (path text_pattern_ops);
Masalah operasional yang ditimbulkan lock dan transaksi
-
Lock di Postgres
- Lock atau mutex adalah mekanisme yang membuat operasi berisiko hanya dijalankan oleh satu klien pada satu waktu
- Di database, pembaruan objek seperti row, table, atau view harus berhasil seluruhnya atau gagal seluruhnya; untuk mencegah situasi akibat operasi bersamaan yang hanya berhasil sebagian, lock diperoleh pada objek terkait
- Level lock tabel di Postgres memiliki beberapa tingkatan, dari yang kurang membatasi hingga yang lebih membatasi
ACCESS SHARE:SELECTROW SHARE:SELECT ... FOR UPDATEROW EXCLUSIVE:UPDATE,DELETE,INSERTSHARE UPDATE EXCLUSIVE:CREATE INDEX CONCURRENTLYSHARE:CREATE INDEX, tetapi bukanCONCURRENTLYACCESS EXCLUSIVE: banyak bentukALTER TABLE,ALTER INDEX
- Pada satu tabel, operasi berikut bisa berjalan atau harus menunggu
SELECTsaatUPDATE: bisaCREATE INDEX CONCURRENTLYsaatUPDATE: bisaCREATE INDEXsaatSELECT: bisaALTER TABLEsaatSELECT: umumnya menungguSELECTsaatALTER TABLE: umumnya menunggu
- Beberapa bentuk
ALTER TABLEmungkin memerlukan lock yang lebih lemah, dan informasi lengkap dapat dilihat di dokumentasi resmi explicit locking serta panduan konflik lock per operasi
-
ALTER TABLE yang lambat dan antrean lock
- Jika
ALTER TABLEberlangsung lama,SELECTyang membaca tabel yang sama juga dapat terblokir - Jika itu tabel inti seperti
usersyang dirujuk semua request aplikasi web, request dapat menunggu hingga timeout dan mengembalikan 503 - Penyebab umum
ALTER TABLEyang lambat antara lain- Menambahkan kolom dengan default non-konstan
- Mengubah tipe kolom
- Menambahkan uniqueness constraint
- Sejak Postgres 11, masalah semua default membuat penambahan kolom menjadi lambat sudah diperbaiki; default non-konstanlah yang bisa menjadi masalah
- Meski
ALTER TABLEsendiri adalah operasi cepat, ia tidak akan berjalan sebelum mendapatkan lock- Jika ada
SELECTlambat dari dashboard internal lama yang sedang berjalan,ALTER TABLEharus menunggu
- Jika ada
- Karena lock Postgres membentuk antrean, query berikutnya ke tabel yang sama yang masuk setelah
ALTER TABLEyang sedang menunggu juga dapat ikut menunggu - Skenario yang sama dapat dipelajari lebih lanjut di Migrations and exclusive locks
- Jika
-
Transaksi jangka panjang juga berbahaya
- Transaksi adalah cara menggabungkan beberapa statement database menjadi all-or-nothing, dimulai dengan
BEGINdan diakhiri denganCOMMIT - Perubahan di dalam transaksi tidak terlihat oleh klien lain, dan dipublikasikan ke database saat
COMMIT - Ini cocok untuk operasi seperti transfer uang, ketika pengurangan saldo satu akun dan penambahan saldo akun lain harus berhasil bersama atau dibatalkan bersama
- Jika transaksi mendapatkan lock, lock tersebut dipertahankan hingga
COMMIT - Jika setelah
BEGINseseorang melakukanUPDATEpada row tertentu lalu pergi,DELETEatas row tersebut oleh klien lain akan berhenti sampai transaksi itu di-commit - Transaksi yang terbuka lebih lama dari yang diperlukan dapat memblokir query atau update dari klien lain
- Transaksi adalah cara menggabungkan beberapa statement database menjadi all-or-nothing, dimulai dengan
JSONB adalah alat yang tajam
-
Masalah performa dan skema pada JSONB
- JSONB fleksibel, tetapi jika digunakan keliru, kekurangannya besar
- Postgres tidak melacak statistik kolom JSONB, sehingga query kesetaraan pada satu kolom JSONB bisa jauh lebih lambat daripada query pada kumpulan kolom biasa
- Dalam satu kasus, Anda dapat melihat contoh menjadi 2000 kali lebih lambat karena JSONB
- Kolom JSONB secara praktis dapat berisi apa saja, sehingga kuat, tetapi jaminan atas strukturnya kecil
- Pada tabel biasa, Anda dapat melihat skema dan memprediksi hasil query, tetapi pada JSONB tidak pasti apakah nama key memakai camelCase atau snake_case, apakah status berupa boolean atau enum
- Karakteristik tipe statis yang dimiliki data Postgres biasa tidak berlaku pada JSONB dengan cara yang sama
-
Kejanggalan perbandingan tipe JSONB
- Saat ingin menemukan baris dengan field
brandbernilaiJanSportdi kolom JSONBdatapada tabelbackpacks, query berikut tidak bekerja
select * from backpacks where data['brand'] = 'JanSport';- Postgres mengharapkan tipe di sisi kanan perbandingan cocok dengan tipe di sisi kiri, dan sisi kanan harus merupakan dokumen JSON yang valid
- Dokumen JSON harus berupa objek, array, string, angka, boolean, atau null, sehingga
JanSportsaja bukan JSON yang valid - Query yang benar adalah membandingkan dengan string JSON, atau mengubah sisi kiri menjadi
textPostgres
select * from backpacks where data['brand'] = '"JanSport"'; select * from backpacks where data['brand'] = '"JanSport"'::jsonb; select * from backpacks where data->>'brand' = 'JanSport';NULLdi SQL dannulldi JSONB berperilaku berbeda'null'::jsonb = 'null'::jsonbadalahtrue, tetapiNULL = NULLadalahNULL
- JSONB memiliki banyak operator dan fungsi khusus, sehingga sulit diingat sekaligus
- Postgres memiliki
JSON, yang menyimpan nilai JSON sebagai teks, danJSONB, yang mengubahnya menjadi format biner efisien - JSONB memiliki keunggulan seperti dapat diindeks, sementara format JSON bisa dianggap sebagai kasus khusus
- Saat ingin menemukan baris dengan field
2 komentar
Saya harus membacanya suatu saat nanti tentang hal-hal yang tidak boleh dilakukan.
Opini Hacker News
PostgreSQL pada umumnya peka huruf besar/kecil, tetapi menulis kata kunci SQL dengan huruf besar biasanya merupakan upaya meningkatkan keterbacaan lewat pencocokan pola visual
Itu tidak wajib, tetapi kalau harus men-debug query orang lain, saya mungkin akan memasukkannya ke prettifier agar bisa cepat menelusuri definisinya tanpa tersandung detail kecil bentuk sintaks
Seperti merapikan kode di bahasa lain, struktur visual seperti indentasi yang konsisten mengurangi waktu untuk memahami bagian-bagian yang sudah jelas dan membuat kita fokus pada hal penting
Namun saya benar-benar tidak suka ketika identifier benar-benar mencampur huruf besar/kecil seperti
actuallyUsingCaseInIdentifiers, dan saya tidak ingin melihat kolom yang perlu tanda kutip ganda hanya untuk diperiksa di CLIKalau hanya mengetik cepat query sementara yang tidak akan dilihat siapa pun lalu membuangnya, saya tidak memedulikan huruf besar/kecil; tetapi SQL yang di-commit ke repositori saya tulis perintahnya dengan ALL CAPS
Sekarang dengan adanya warna itu tidak lagi diperlukan, tetapi ini hanya ingatan lama dan saya tidak punya rujukan pendukung
Meski begitu, identifier yang diberi tanda kutip dan yang tidak sebaiknya tidak dicampur, dan introspeksi struktur internal pun umumnya tidak distandardisasi, jadi tidak terlalu bermakna
Saya baru pertama kali melihat bagian “don’t do this” di wiki PostgreSQL, dan itu cukup berguna: https://wiki.postgresql.org/wiki/Don%27t_Do_This
Misalnya, untuk skema baru, fitur seperti pewarisan tabel dinonaktifkan, dan untuk mengaktifkannya kembali harus sengaja memakai konfigurasi yang rumit, tampaknya lebih masuk akal
Banyak hal yang disebutkan di sini tidak hanya berlaku untuk PostgreSQL
Contohnya perilaku aneh
NULLdan urutan kolom indeks; khususnya interaksi antara NULL dan indeks/constraint unik juga tidak intuitif di MySQLMisalnya, jika pada tabel pengguna
emailtidak boleh NULL danusernameboleh NULL, lalu diberi constraint unik(email, username), Anda bisa memasukkanemailyang sama berkali-kali ketikausernamebernilai NULL. Sebab NULL tidak sama dengan NULL lainhttps://www.postgresql.org/docs/devel/sql-createtable.html#S...
Kasus penggunaan yang membutuhkan perilaku sebaliknya jauh lebih jarang
Tidak cukup hanya mengatakan “normalisasikan data kalau tidak ada alasan yang baik” lalu berhenti di situ
Di halaman yang ditautkan penulis pun ada 11 bentuk normalisasi, termasuk bentuk tidak ternormalisasi; kebanyakan orang bahkan tidak tahu itu apa, dan 7 di antaranya tidak akan pernah dipakai
Jangan membuat orang berkeliaran mencari bentuk normal yang lebih tinggi
Di proyek yang baru-baru ini saya pindahi, saya juga harus memperbaiki beberapa masalah seperti ini; hampir tidak ada alasan untuk menduplikasi data
Tip pertama adalah menjalankan VACUUM setiap hari
Saat pertama mulai, saya tidak tahu hal ini sehingga sama sekali tidak menjalankan VACUUM pada database reddit, dan suatu hari ketika akhirnya terpaksa menjalankannya, reddit hampir seharian down sambil menunggu prosesnya selesai
Untuk skala reddit, mengejutkan bahwa transaction ID tidak habis lebih dulu
Saya berharap para developer lebih memperhatikan normalisasi, dan berhenti memasukkan semuanya ke kolom JSONB
Developer yang lebih berpengalaman tahu bahwa jawaban yang benar adalah tidak menduplikasi apa pun selain key, dan melakukan denormalisasi hanya kalau benar-benar terpaksa
Kemudian database seperti Mongo muncul, menawarkan “sesuatu yang mirip database” tempat normalisasi sulit atau tidak bermakna, sehingga mendorong para junior itu; akibatnya, desain database yang mengerikan dan menara sampah yang mustahil dipelihara sempat berkembang pesat
Kini bandulnya sudah berayun kembali dan kita menemukan lagi manfaat database yang ternormalisasi, tetapi kolom JSON masih menjadi celah tempat praktik buruk bisa tumbuh
Pertama, untuk menyimpan JSON. Saat web server memanggil API pihak ketiga, menyimpan respons API mentah ke kolom JSONB lalu memprosesnya dari sana meninggalkan catatan yang bisa diaudit ketika men-debug masalah yang berasal dari API tersebut
Kedua, untuk menyimpan sum type. Tidak adanya dukungan sum type di SQL bisa dibilang salah satu kekurangan terbesar saat memodelkan data di database SQL
Ada berbagai workaround, dan “masukkan saja ke kolom JSONB lalu validasi di aplikasi” adalah salah satunya, tetapi tidak ada workaround yang benar-benar istimewa bagus
Selama Anda tidak menulis query buruk di dalamnya tanpa mengangkat nilai di JSONB ke kolom terpisah, menurut saya itu sendiri bukan masalah besar
Sebab, selama kebutuhan persistensi berhasil dipenuhi, tidak ada tekanan kuat untuk memikirkan desain yang baik
Apakah masuk akal membuat DBMS lagi di atas DBMS, itu bisa dipertanyakan, tetapi bagaimanapun begitulah keadaan saat ini
Jika kolom baru merusak performa atau menimbulkan masalah, harus bisa dikembalikan
Jika ada tool CLI yang terlibat, Anda juga harus menangani seberapa banyak downtime yang dapat ditoleransi, apakah update versi yang tersinkronisasi di seluruh perusahaan memungkinkan, atau apakah perlu mendukung skema lama dan baru sekaligus untuk sementara waktu
Jika database bukan bagian dari produk utama tim, semua hal ini bisa saja tidak ada
Saya menulis artikel ini untuk membantu pemula: https://tomcam.github.io/postgres/
Artikelnya sangat bagus, dan saya tidak tahu dokumentasi PostgreSQL sampai 3200 halaman
Saya sudah memakainya cukup lama dan belajar saat diperlukan; saya juga cukup menyukai dokumentasi resminya, dan suka membaca tulisan terkait ketika membutuhkan topik tertentu
Sepertinya akan membantu pembaca jika penulis menambahkan di https://challahscript.com/what_i_wish_someone_told_me_about_... bahwa indeks kolom
(b, a)bekerja dengan baik saat query hanya memakaibIni sedikit tersirat ketika membahas query yang hanya memakai
a, tetapi tidak ada salahnya dibuat lebih eksplisitBagian JSON/JSONB tidak banyak saya lihat karena saya hampir tidak menggunakannya
Mengingat SQL konyol yang pernah saya lihat di lapangan, sebaiknya mulai dari membaca makalah Codd dan memahami apa itu model relasional
Hanya 11 halaman, dan dengan membacanya saja penderitaan di dunia ini akan berkurang
Hampir semua isi artikel ini juga berlaku untuk database MVCC lain seperti MySQL
Detailnya bisa berbeda, tetapi MySQL juga mengalami masalah serupa yang menarik, seperti menderita akibat transaksi panjang dan mengambil metadata lock saat
ALTER