Tips dan Trik SQL
(github.com/ben-n93)- Ini adalah daftar kebiasaan penulisan SQL dan pola kueri yang sering dipakai dalam pekerjaan analisis data, dengan catatan bahwa semuanya mungkin tidak berlaku sama di semua RDBMS
- Dari sisi keterbacaan, disarankan memakai koma di awal,
WHERE 1=1, indentasi, CTE, komentar, danUSINGagar kueri lebih mudah dibaca dan diubah - Untuk pemrosesan data, contoh mencakup sintaks yang dipakai dalam praktik untuk memfilter hasil, membuat total, dan memeriksa perbedaan tabel, seperti anti-join,
QUALIFY,GROUP BY ROLLUP, danEXCEPT - Dari sisi performa dan akurasi,
NOT INyang bercampurNULL, konversi tipe implisit, serta konflik alias pada field hasil kalkulasi dapat memengaruhi hasil kueri atau kecepatannya - Dalam kueri yang kompleks, kebiasaan dasar seperti memahami urutan eksekusi, memeriksa dokumentasi, memperjelas asal kolom, dan memberi nama kueri tersimpan berperan penting untuk meningkatkan debugging dan reusability
Kebiasaan untuk Meningkatkan Keterbacaan Penulisan SQL
- Repositori ini adalah daftar tips dan trik SQL yang dikumpulkan selama bertahun-tahun, berfokus pada hal-hal yang berguna dalam pekerjaan sehari-hari analis data dan hal-hal yang seandainya diketahui saat pertama kali menulis SQL
- Beberapa tips mungkin tidak cocok untuk semua RDBMS
-
Koma di awal dan
ANDdi awal- Untuk memisahkan field pada klausa
SELECT, disarankan memakai koma di awal daripada koma di akhir - Ini membuat lebih jelas apakah sesuatu adalah kolom baru atau kode yang hanya pindah baris
- Lebih mudah menemukan koma yang hilang meski panjang baris berbeda-beda
- Dengan alasan yang sama,
ANDjuga bisa diletakkan di awal sebelum kondisi dalam klausaWHERE
- Untuk memisahkan field pada klausa
-
Mempermudah pengujian kondisi dengan
WHERE 1=1- Jika menambahkan kondisi dummy
1=1ke klausaWHERE, kueri tidak rusak meski kondisi dikomentari saat pengujian - Bahkan jika semua kondisi dikomentari,
1=1tetap tersisa sehingga kueri masih dapat dijalankan
- Jika menambahkan kondisi dummy
-
Indentasi dan formatter
-
Pertimbangkan CTE untuk kueri kompleks
- Jika inline view ditumpuk lebih dari 2–3 tingkat, kueri mudah menjadi sulit dipahami saat dilihat lagi beberapa minggu kemudian
- CTE disajikan sebagai cara untuk membuat kueri panjang menjadi lebih terstruktur, sekaligus membantu reusability dan debugging
-
Komentar harus menjelaskan “mengapa”
- Setelah waktu berlalu, bisa jadi sulit mengingat mengapa pemrosesan tertentu dilakukan
- Secara umum, komentar sebaiknya menjelaskan mengapa sesuatu dilakukan, bukan bagaimana kode bekerja
- Contohnya adalah menambahkan komentar pada kondisi yang mengecualikan konten archive karena CMS baru tidak dapat menangani format video archive
-
Gunakan
USINGuntuk join kolom bernama sama- Saat melakukan join pada dua tabel menggunakan kolom dengan nama yang sama,
USINGdapat mengekspresikan join dengan lebih ringkas daripadaON USINGmenghapus duplikasi kolom umum dari hasil dan hanya mengembalikan satu kolom- Jika menggunakan
ONdan tidak menyebutkan kolom umum secara eksplisit, dapat terjadi errorambiguous column name
- Saat melakukan join pada dua tabel menggunakan kolom dengan nama yang sama,
Sintaks yang Berguna untuk Pemrosesan Data
-
Menemukan baris yang tidak ada di tabel lain dengan anti-join
- Anti-join digunakan untuk mengembalikan baris yang ada di satu tabel tetapi tidak memiliki pasangan di tabel lain
- Contohnya membahas situasi mengambil hanya
video_iddari konten yang tidak di-archive - Ada beberapa cara implementasi
- Melakukan
LEFT JOIN, lalu memfilter hanya baris dengan key dari tabel pasangan bernilaiNULL - Menggunakan
NOT INdan subkueri - Menggunakan
NOT EXISTSdan subkueri terkorelasi NOT INtidak disarankan karena dapat tidak bekerja sesuai harapan akibat nilaiNULL
-
Memfilter hasil window function dengan
QUALIFYQUALIFYmemungkinkan hasil kueri difilter berdasarkan hasil window function- Karena bisa memfilter tanpa inline view, jumlah baris kode dapat dikurangi
- Contohnya memilih 10 pasar teratas per produk dengan
DENSE_RANK(), lalu memfilternya denganQUALIFY - Ada batasan bahwa
QUALIFYtampaknya hanya tersedia di data warehouse besar seperti Snowflake, Amazon Redshift, dan Google BigQuery
-
GROUP BYdanORDER BYberbasis posisi kolom- Alih-alih nama kolom, Anda bisa menulis berdasarkan posisi kolom seperti
GROUP BY 1danORDER BY 2 - Ini bisa berguna untuk kueri sementara atau sekali pakai
- Untuk kode produksi, disarankan selalu merujuk langsung ke nama kolom
- Alih-alih nama kolom, Anda bisa menulis berdasarkan posisi kolom seperti
-
Membuat total dengan
GROUP BY ROLLUPGROUP BY ROLLUPdapat digunakan untuk membuat subtotal dan total keseluruhan- Contohnya membuat baris total keseluruhan gaji sambil menghitung jumlah gaji per departemen
- Dokumentasi Transact-SQL menjelaskan bahwa
ROLLUPmembuat grup untuk setiap kombinasi ekspresi kolom, mengurangi jumlah grup dari kanan ke kiri, lalu membuat subtotal dan total keseluruhan - Dengan menerapkan
COALESCE, baris total keseluruhan dapat ditampilkan sepertiTotal - Perlu memperhatikan kolom pengurutan agar baris total keseluruhan muncul di bagian bawah hasil
-
Menemukan perbedaan dua result set dengan
EXCEPTEXCEPTmengembalikan baris yang ada di hasil kueri pertama tetapi tidak ada di hasil kueri kedua- Jika
EXCEPTdanUNION ALLdigunakan bersama, Anda dapat memverifikasi apakah dua tabel memiliki data yang sama - Jika tidak ada baris yang dikembalikan, kedua tabel identik
- Jika ada baris yang dikembalikan, baris-baris itulah penyebab perbedaannya
Pola yang Merusak Performa dan Akurasi
-
Pada kolom yang bisa
NULL,NOT EXISTSlebih baik daripadaNOT IN- Jika kolom pembanding mengizinkan
NULL,NOT INbiasanya bisa lebih lambat daripadaNOT EXISTS - Fenomena ini dialami di Snowflake, dan Don’t Do This di PostgreSQL Wiki menyebutkan bahwa
NOT IN (SELECT ...)tidak dioptimalkan dengan baik NOT INtidak bekerja sesuai harapan jika nilai pembanding memilikiNULL- Kolom yang mengizinkan
NULLtidak berarti benar-benar ada nilaiNULL, tetapi saat menangani tabel yang tidak dapat diubah,NOT EXISTSdapat membantu meningkatkan kecepatan
- Jika kolom pembanding mengizinkan
-
Konversi tipe implisit bisa memperlambat atau gagal
- Jika nilai dengan tipe data berbeda dari kolom dimasukkan ke kondisi, database dapat mencoba konversi tipe implisit
- Contohnya membahas perbandingan integer
200050dengan kolomvideo_idbertipe string - Mengandalkan konversi tipe implisit dapat menimbulkan masalah
- Jika ada nilai yang tidak dapat dikonversi, error dapat terjadi
- Kueri bisa menjadi lebih lambat karena pekerjaan tambahan untuk mengonversi setiap nilai ke tipe yang ditentukan
- Gunakan tipe data yang sama dengan kolom, atau gunakan fungsi seperti
TRY_TO_NUMBERmilik Snowflake untuk menghindari error - Dampak terhadap kecepatan bergantung pada ukuran dataset yang diproses
Kesalahan yang Sering Terjadi
-
NOT INdanNULLNOT INtidak bekerja jika nilai pembanding memilikiNULL- Karena
NULLmerepresentasikan Unknown, engine SQL tidak dapat memverifikasi bahwa nilai yang diperiksa tidak ada dalam daftar - Dalam kasus ini, menggunakan
NOT EXISTSadalah alternatifnya
-
Konflik alias pada field hasil kalkulasi
- Jika nama field hasil kalkulasi dibuat sama dengan kolom yang sudah ada, perilaku yang tidak terduga dapat muncul
- Dokumentasi
GROUP BYSnowflake menyebutkan bahwa jika nama dalam klausaGROUP BYcocok dengan nama kolom dan alias sekaligus, nama kolom yang digunakan - Pada contoh, jika membuat alias dengan
LEFT(product, 1) AS productlalu menulisGROUP BY product, pengelompokan dilakukan berdasarkan kolomproductasli, bukan huruf pertama, sehingga 3 baris dikembalikan - Ada dua solusi
- Gunakan alias unik seperti
product_letter - Tulis ekspresinya secara eksplisit seperti
GROUP BY LEFT(product, 1) - Masalah alias juga dapat terjadi pada window function
- Dalam contoh, revenue untuk
Robotdiubah menjadi 0 denganCASE, tetapi karena diterapkan setelah window function dijalankan, peringkat yang dihasilkan berbeda dari ekspektasi - Jika memungkinkan, gunakan alias unik atau masukkan ekspresi kalkulasi langsung ke dalam
ORDER BYpada window function
-
Jelaskan kolom berasal dari tabel mana
- Dalam kueri kompleks dengan banyak join, masalah nilai harus bisa dilacak sampai ke tabel sumber
- Jika dua tabel berbagi nama kolom yang sama dan asal kolom tidak ditulis, RDBMS dapat mengeluarkan error
- Contohnya memperjelas asal kolom dengan menambahkan alias tabel seperti
vc.video_iddanmetadata.season
Urutan Eksekusi, Dokumentasi, dan Nama Tersimpan
-
Memahami urutan eksekusi SQL
- Salah satu nasihat paling penting bagi orang yang belajar SQL adalah memahami urutan eksekusi klausa
- Mengetahui urutan eksekusi dapat sangat mengubah cara menulis kueri
- Sebagai referensi, diberikan A beginner’s guide to the true order of SQL operations
-
Baca dokumentasi sampai akhir
- Ada kasus penggunaan
GREATEST()di Snowflake untuk mengembalikan tanggal terbaru dari beberapa kolom tanggal GREATEST()mengembalikanNULLjika salah satu argumennya adalahNULL- Jika dokumentasi dibaca lebih lanjut,
GREATEST_IGNORE_NULLS()bisa digunakan alih-alihCOALESCE(GREATEST(...), ...) - Dalam banyak kasus, memindai dokumentasi hanya membutuhkan kurang dari 1 menit dan dapat mengurangi upaya mencari penyebab perilaku yang berbeda dari ekspektasi
- Ada kasus penggunaan
-
Gunakan nama deskriptif untuk kueri tersimpan
- Untuk menghindari situasi tidak dapat menemukan kueri yang perlu dijalankan ulang atau dijadikan referensi, sebaiknya simpan dengan nama deskriptif
- Nama tersimpan biasanya mencakup topik kueri, bulan eksekusi, dan nama peminta
- Contohnya adalah format
Lapsed users analysis - 2023-09-01 - Olivia Roberts
2 komentar
Koma di awal pada postingan ini semuanya ditulis sebagai koma di akhir. Di teks aslinya, koma dimasukkan di awal.
Komentar Hacker News
Tip yang ingin saya tambahkan: pelajari server DB dengan benar dan sering-sering periksa execution plan. Hasilnya bisa mengejutkan, jadi sebaiknya sesuaikan lalu cek lagi
Biasanya
EXISTSlebih cepat daripadaIN, danNOT EXISTSberperilaku berbeda dariEXCEPTdalam penangananNULL. Alih-alih menggabungkan tabel lalu menyaring baris dengan sesuatu sepertiDISTINCT, terkadang jauh lebih cepat memakai kolom subkueri di daftarSELECT. Ini tetap bisa berlaku meski mengambil lebih dari 10 nilai dari tabel yang sama, dan meski server DB mendukung lateral join. Namun subkueri harus mengembalikan paling banyak satu barisKueri yang bukan sekali pakai harus dibuat agar tidak melakukan pemindaian seluruh tabel. Pemindaian tabel hari ini bisa menjadi insiden besok, jadi tambahkan indeks. Ingat juga bahwa klausa
GROUP BYbiasanya menentukan penggunaan indeksJika harus memfilter berdasarkan ekspresi, misalnya memeriksa apakah substring sama dengan nilai tertentu, Anda bisa menambahkan kolom terhitung dan memasang indeks di sana. Beberapa DB mendukung indeks ekspresi secara langsung. Menggunakan
UNION ALLalih-alihORsering kali bisa jauh lebih cepat, termasuk pada kueri kompleks atau banyak kondisiORSaat DB tidak cukup pintar menentukan urutan pemfilteran, men-
JOINsubkueri untuk memaksakan urutannya juga bergunaHal yang menarik di Postgres—dan mungkin juga berlaku di DB lain—adalah operasi
INSERT (SELECT ...)bisa menjadi hampir lebih cepat secara linear jika di-sharding manual sesuai jumlah core CPU. Ini bisa dilakukan bahkan dengan sekitar 10 join. Pertama lihatEXPLAIN, cari join yang paling dalam atau paling luar, lalu jalankan kueri paralel terpisah untuk tiap rentang baris (id >= start AND id < end). Dalam pekerjaan 6 tahun lalu, saya banyak memakai cara ini karena alasan yang aneh. Postgres 10+ sudah menambahkan paralelisme, tetapi setahu saya belum semaju iniSELECTMisalnya, jika menjalankan
SELECT column1, (SELECT column2, column3, ... FROM table_b WHERE table_a.id = table_b.a_id) FROM table_a, hasilnya seperti yang diduga: “subquery must return only one column”. Apakah maksudnya mengembalikan beberapa kolom sebagai record/tipe komposit?Alasan mengapa klausa
GROUP BYbiasanya menentukan penggunaan indeks tidak langsung terasa jelas bagi saya, tetapi bagi yang penasaran, artikel ini menjelaskannya langkah demi langkah dengan baik: https://www.brentozar.com/archive/2015/06/indexing-for-group...EXPLAIN, dan pelajari cara menafsirkannya dengan alat pilihan Anda. Kueri juga harus dimonitorDi startup sebelumnya, kami memasang PgHero, dan itu sangat membantu untuk optimasi performa serta penentuan prioritas
Ada kueri yang strategi akses paling efisiennya memang pemindaian seluruh tabel. Biasanya kueri analitik/agregasi yang membaca seluruh tabel seperti itu, dan kadang bahkan saat mengambil hanya 50% dari seluruh baris, table scan tetap lebih baik.
Saya juga kurang paham bagaimana pemindaian tabel read-only bisa berujung pada insiden. Itu karena tidak memblokir akses bersamaan. Kekurangannya hanya beban I/O yang besar, tetapi jika server tidak sanggup menangani beban sebesar itu, menurut saya sejak awal spesifikasinya memang sangat kurang
Tiga contoh di bagian “readability” terasa aneh. Dua yang pertama secara harfiah mengorbankan keterbacaan demi kemudahan menulis, dan yang terakhir adalah bentuk monster yang sulit dibaca dan hampir tidak tertolong bahkan dengan indentasi
Menurut saya developer melihat riwayat commit sama seringnya dengan kode sumber aktual
Karena sudah cukup sering melihatnya, sekarang tidak terlalu mengganggu lagi
Saya juga tidak melihat masalah
Dan sepertinya tidak ada yang salah
SELECTper baris, tetapi membiarkan baris sepanjang 150 karakter begitu saja? Ini definisi keterbacaan yang rusak. Soal koma bahkan belum sempat saya mulaiDalam code review, baris panjang tidak benar-benar dilihat orang. Itu masalah terbesar AngularJS. Merge salah ditangani dan semuanya rusak, karena sekitar kolom ke-90 mata mulai kabur. Saya sudah berada di lebih dari setengah lusin tim yang memakai code review, dan selalu sama. Bahkan ketika sangat sadar akan masalah ini dan berusaha menghindarinya, saya sendiri masih membuat kesalahan dengan frekuensi sekitar setengah dari orang lain
Pecah-pecahlah barisnya. Apalagi kalau menunjukkan contoh kepada orang lain
Tips saat menangani stored procedure yang kompleks adalah sebagai berikut
WHERE. Jika mencobaJOINatau operasi kompleks, kemungkinan besar akan timeoutJika melakukan poin 5 tanpa poin 6, besar kemungkinan Anda tidak akan melihat bahwa Anda sedang melakukan sesuatu yang tidak optimal. Saran saya adalah hindari optimasi dini: tulis dulu dengan cara yang paling intuitif, lalu optimalkan hanya saat diperlukan. Yang terpenting adalah jangan menulis SQL secara prosedural. Anda mendeskripsikan data yang diinginkan, bukan memerintahkan engine bagaimana cara mengambilnya
Di sisi lain, Microsoft terus memasang peringatan agar kita tidak mencoba melakukan tuning terhadapnya, seolah-olah query planner-lah yang paling tahu
Versi DB juga bisa berpengaruh
Saya tidak suka pengembangan “untuk berjaga-jaga”. Begitu juga dengan interface, maupun placeholder seperti
where 1=1Lakukan saat diperlukan. Jangan melakukannya hanya karena mungkin suatu hari di masa depan akan dibutuhkan. Kode produksi bukan tempat untuk meninggalkan helper pengembangan. Saat pengembangan, silakan lakukan sesuka hati, tetapi dalam kode produksi, keterbacaan dan maksud yang jelas jauh lebih penting
Satu tambahan tentang “anti join”. Jika hanya ingin memeriksa apakah baris yang memenuhi kondisi ada di tabel besar lain atau subquery, sebaiknya gunakan EXISTS alih-alih
INatauLEFT JOINEXISTSmengembalikan true segera setelah menemukan item yang cocok. PadaLEFT JOINdanIN, engine mengumpulkan semua hasil sebelum evaluasi(NOT) EXISTSmenghasilkan execution plan yang lebih baik daripada(LEFT) JOINatau(NOT) IN, atau menghasilkan plan yang samaSelain itu, niatnya juga lebih jelas
Terkait “beri komentar pada kode”, setidaknya di MSSQL sering disarankan memakai
/**/alih-alih--untuk komentar. Ini karena fitur seperti Query Store sering menyimpan query tanpa baris baru, sehingga ketika query diambil dari sana, kita harus memperbaiki semuanya secara manual alih-alih langsung memakai formatter IDEOBJECT_DEFINITIONselect name,cast((select OBJECT_DEFINITION(object_id) for xml path('')) as xml) from sys.proceduresKarena baris baru dipertahankan, mungkin lebih mudah untuk merapikannya. Namun karakter XML lain akan rusak, seperti
>berubah menjadi>. Pilihan lain adalah memakaiVARBINARYdan sesuatu untuk mengurainya kembaliSemua orang ribut soal saran koma, tetapi apakah
1=1di klausaWHEREdianggap ide bagus? Kalau melihat itu dalam code review, saya tidak tahu harus berpikir apa tentang penulisnyaWHEREtidak memengaruhi baris lain sehingga code review menjadi lebih mudahNamun jika alasannya untuk menambahkan kondisi dinamis seperti dalam kasus ini, di tempat saya bekerja orang itu pasti akan dipecat
Apakah ada yang bisa berbagi panduan umum tentang di mana sebaiknya menarik garis antara meningkatkan kecepatan lewat konfigurasi DB—cara yang hampir seperti “membeli”—dan “membangun” yang pada dasarnya diimplementasikan secara manual? Dari pengalaman saya yang terbatas, DBA yang kompeten mendapat bayaran jauh lebih tinggi dan bekerja di tempat lain, sehingga pekerjaan ini sering jatuh ke developer aplikasi. Seperti disebutkan di atas, memahami DB itu penting
Contoh yang representatif adalah data yang menumpuk dalam jumlah besar seiring waktu, dan data terbaru paling sering diakses. DBA bisa menjaga akses tetap cepat dengan partitioning atau partial index, tetapi developer aplikasi juga bisa memindahkan record ke tabel arsip terpisah di belakang layar sambil tetap mendukung fitur seperti pencarian akhir atas seluruh dataset. Saya juga merasa pekerjaan awal untuk memecah satu tabel menjadi beberapa tabel pada waktu yang tepat bisa cukup diautomasi oleh tool, misalnya ketika terbatas oleh kurangnya fitur pada cloud DB
Pilihan pengelolaan lain adalah menyimpan semua blob/file besar di database terpisah, atau di file system, untuk memakai konfigurasi storage yang berbeda. Ini juga bisa ditangani oleh DB maupun secara manual
Dalam kasus ekstrem, sepertinya bisa sampai mengimplementasikan index sendiri. Caranya dengan memiliki tabel raksasa berisi satu primary key auto-increment dan banyak sekali kolom, lalu membuat tabel terpisah yang berisi ID tersebut dan beberapa kolom yang dapat dicari. Bahkan bisa saja sampai ke full-text search atau vector
Tips yang berguna saat mengimplementasikan pola materialized view secara manual di MSSQL 2016+ adalah menggunakannya bersama partition switching. Ini dijelaskan dan diimplementasikan dengan baik di https://github.com/cajuncoding/SqlBulkHelpers?tab=readme-ov-.... Itu adalah library kecil yang saya temukan secara kebetulan; paling berguna secara komersial, tetapi peringkat pencariannya rendah dan bintangnya sedikit, serta berfokus pada bulk insert ke MSSQL dengan .NET. Saya pikir ini contoh yang bagus tentang menarik garis beli/bangun dengan tepat melalui otomatisasi partition switching
Yang terlewat: harus berhenti memakai
SELECT *. Hampir pasti Anda tidak membutuhkan seluruh lebar tabel, dan dengan begitu Anda menambah data yang harus difilter dan dikirim, sekaligus menghalangi fitur keren bernama semi joinKalau Anda developer, benar.
SELECT *punya jebakan, dan hampir selalu sebaiknya menyebutkan kolom secara eksplisit, atau memakai query builder yang melakukannya untuk AndaTapi kalau Anda analis, hidup ini singkat, dan kadang Anda tidak ingin mengetik semua kolom satu per satu.
SELECT *juga tidak apa-apaMungkin agak keluar dari topik, tetapi apakah bisa diterima jika maintainer langsung menutup pull request tanpa komentar atau diskusi apa pun?
Saya bertanya sebagai orang yang sesekali pernah berkontribusi, atau mencoba berkontribusi, ke repository
Contoh: https://github.com/ben-n93/SQL-tips-and-tricks/pulls?q=is%3A...