SQLite adalah database relasional populer yang Anda sematkan ke dalam aplikasi Anda. Namun, ada banyak jebakan dan jebakan yang harus Anda hindari. Artikel ini membahas beberapa jebakan (dan cara menghindarinya), seperti penggunaan ORM, cara mendapatkan kembali ruang disk, memperhatikan jumlah maksimum variabel kueri, tipe data kolom, dan cara menangani bilangan bulat besar.
Pengantar
SQLite adalah sistem database relasional (DB) yang populer . Ini memiliki fitur yang sangat mirip dengan saudaranya yang lebih besar, seperti MySQL , yang merupakan sistem berbasis klien/server. Namun, SQLite adalah tertanam basis data . Itu dapat dimasukkan dalam program Anda sebagai perpustakaan statis (atau dinamis). Ini menyederhanakan penerapan , karena tidak diperlukan proses server terpisah. Binding dan pustaka pembungkus memungkinkan Anda mengakses SQLite di sebagian besar bahasa pemrograman .
Saya telah bekerja dengan SQLite secara ekstensif saat mengembangkan BSync sebagai bagian dari disertasi PhD saya. Artikel ini adalah daftar (acak) jebakan dan jebakan yang saya temukan selama pengembangan . Saya harap Anda akan menemukan mereka berguna dan menghindari membuat kesalahan yang sama seperti yang pernah saya lakukan.
Jebakan dan jebakan
Gunakan perpustakaan ORM dengan hati-hati
Pustaka Object-Relational Mapping (ORM) mengabstraksi detail dari mesin basis data konkret dan sintaksnya (seperti pernyataan SQL tertentu) ke API berorientasi objek tingkat tinggi. Ada banyak perpustakaan pihak ketiga di luar sana (lihat Wikipedia). Perpustakaan ORM memiliki beberapa keunggulan:
- Mereka menghemat waktu selama pengembangan , karena mereka dengan cepat memetakan kode/kelas Anda ke struktur DB,
- Mereka seringkali lintas platform , yaitu, izinkan penggantian teknologi DB konkret (misalnya SQLite dengan MySQL),
- Mereka menawarkan kode pembantu untuk migrasi skema .
Namun, mereka juga memiliki beberapa kelemahan parah Anda harus menyadari:
- Mereka membuat bekerja dengan database muncul mudah . Namun, pada kenyataannya, mesin DB memiliki detail rumit yang harus Anda ketahui . Setelah terjadi kesalahan, mis. saat pustaka ORM memunculkan pengecualian yang tidak Anda pahami, atau saat kinerja run-time menurun, waktu pengembangan yang Anda hemat dengan menggunakan ORM akan cepat habis oleh upaya yang diperlukan untuk men-debug masalah . Misalnya, jika Anda tidak tahu apa indeks adalah, Anda akan kesulitan memecahkan masalah kemacetan kinerja yang disebabkan oleh ORM, ketika ORM tidak secara otomatis membuat semua indeks yang diperlukan. Intinya:tidak ada makan siang gratis.
- Karena abstraksi vendor DB konkret, fungsi khusus vendor sulit diakses, tidak dapat diakses sama sekali .
- Ada beberapa overhead komputasi dibandingkan dengan menulis dan mengeksekusi query SQL secara langsung. Namun, menurut saya poin ini diperdebatkan dalam praktiknya, karena biasanya Anda kehilangan performa setelah beralih ke tingkat abstraksi yang lebih tinggi.
Pada akhirnya, menggunakan perpustakaan ORM adalah masalah preferensi pribadi. Jika ya, bersiaplah bahwa Anda harus mempelajari tentang keunikan database relasional (dan peringatan khusus vendor), setelah terjadi kemacetan kinerja atau perilaku yang tidak terduga.
Sertakan tabel migrasi dari awal
Jika Anda tidak menggunakan perpustakaan ORM, Anda harus mengurus migrasi skema DB . Ini melibatkan penulisan kode migrasi yang mengubah skema tabel Anda dan mengubah data yang disimpan dalam beberapa cara. Saya sarankan Anda membuat tabel yang disebut "migrasi" atau "versi", dengan satu baris dan kolom, yang hanya menyimpan versi skema, mis. menggunakan bilangan bulat yang meningkat secara monoton. Ini memungkinkan fungsi migrasi Anda mendeteksi migrasi mana yang masih perlu diterapkan. Setiap kali langkah migrasi berhasil diselesaikan, kode alat migrasi Anda menambah penghitung ini melalui UPDATE
Pernyataan SQL.
Kolom baris yang dibuat otomatis
Setiap kali Anda membuat tabel, SQLite akan secara otomatis membuat INTEGER
kolom bernama rowid
untukmu – kecuali jika Anda memberikan WITHOUT ROWID
klausa (tapi kemungkinan Anda tidak tahu tentang klausa ini). rowid
baris adalah kolom kunci utama. Jika Anda juga menentukan sendiri kolom kunci utama tersebut (misalnya menggunakan sintaks some_column INTEGER PRIMARY KEY
) kolom ini hanya akan menjadi alias untuk rowid
. Lihat di sini untuk informasi lebih lanjut, yang menjelaskan hal yang sama dengan kata-kata yang agak samar. Perhatikan bahwa SELECT * FROM table
pernyataan akan tidak sertakan rowid
secara default – Anda perlu meminta rowid
kolom secara eksplisit.
Verifikasi bahwa PRAGMA
benar-benar bekerja
Antara lain, PRAGMA
pernyataan digunakan untuk mengonfigurasi pengaturan basis data, atau untuk menjalankan berbagai fungsi (dokumen resmi). Namun, ada efek samping yang tidak terdokumentasi dimana terkadang pengaturan variabel sebenarnya tidak berpengaruh . Dengan kata lain, itu tidak berfungsi dan gagal secara diam-diam.
Misalnya, jika Anda mengeluarkan pernyataan berikut dalam urutan yang diberikan, terakhir pernyataan akan tidak memiliki efek apapun. Variabel auto_vacuum
masih memiliki nilai 0
(NONE
), tanpa alasan yang jelas.
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)
Anda dapat membaca nilai suatu variabel dengan menjalankan PRAGMA variableName
dan menghilangkan tanda dan nilai yang sama.
Untuk memperbaiki contoh di atas, gunakan urutan yang berbeda. Menggunakan pengurutan baris 3, 1, 2 akan berfungsi seperti yang diharapkan.
Anda bahkan mungkin ingin menyertakan pemeriksaan tersebut ke dalam produksi kode, karena efek samping ini mungkin bergantung pada versi SQLite konkret dan cara pembuatannya. Pustaka yang digunakan dalam produksi mungkin berbeda dari yang Anda gunakan selama pengembangan.
Mengklaim ruang disk untuk database besar
Secara default, ukuran file database SQLite bertambah secara monoton . Menghapus baris hanya menandai halaman tertentu sebagai gratis , sehingga dapat digunakan untuk INSERT
datanya di masa depan. Untuk benar-benar mendapatkan kembali ruang disk, dan untuk mempercepat kinerja, ada dua opsi:
- Jalankan
VACUUM
pernyataan . Namun, ini memiliki beberapa efek samping:- Ini mengunci seluruh DB. Tidak ada operasi bersamaan yang dapat dilakukan selama
VACUUM
operasi. - Butuh waktu lama (untuk database yang lebih besar), karena secara internal membuat ulang DB dalam file sementara yang terpisah, dan akhirnya menghapus database asli, menggantinya dengan file sementara tersebut.
- File sementara menggunakan tambahan ruang disk saat operasi sedang berjalan. Jadi, bukanlah ide yang baik untuk menjalankan
VACUUM
jika Anda kekurangan ruang disk. Anda masih bisa melakukannya, tetapi harus secara teratur memeriksa(freeDiskSpace - currentDbFileSize) > 0
.
- Ini mengunci seluruh DB. Tidak ada operasi bersamaan yang dapat dilakukan selama
- Gunakan
PRAGMA auto_vacuum = INCREMENTAL
saat membuat DB. Jadikan iniPRAGMA
yang pertama pernyataan setelah membuat file! Ini memungkinkan beberapa pemeliharaan rumah internal, membantu database untuk mendapatkan kembali ruang kapan pun Anda memanggilPRAGMA incremental_vacuum(N)
. Panggilan ini mengklaim kembali hinggaN
halaman. Dokumen resmi memberikan detail lebih lanjut, dan juga kemungkinan nilai lain untukauto_vacuum
.- Catatan:Anda dapat menentukan berapa banyak ruang disk kosong (dalam byte) yang akan diperoleh saat memanggil
PRAGMA incremental_vacuum(N)
:kalikan nilai yang dikembalikan denganPRAGMA freelist_count
denganPRAGMA page_size
.
- Catatan:Anda dapat menentukan berapa banyak ruang disk kosong (dalam byte) yang akan diperoleh saat memanggil
Pilihan yang lebih baik tergantung pada konteks Anda. Untuk file database yang sangat besar, saya merekomendasikan opsi 2 , karena opsi 1 akan mengganggu pengguna Anda dengan beberapa menit atau jam menunggu database dibersihkan. Opsi 1 cocok untuk database yang lebih kecil . Keuntungan tambahannya adalah kinerja DB akan meningkat (yang tidak berlaku untuk opsi 2), karena pembuatan ulang menghilangkan efek samping dari fragmentasi data.
Pikirkan jumlah maksimum variabel dalam kueri
Secara default, jumlah maksimum variabel (“parameter host”) yang dapat Anda gunakan dalam kueri dikodekan hingga 999 (lihat di sini, bagian Jumlah Maksimum Parameter Host Dalam Pernyataan SQL Tunggal ). Batas ini dapat bervariasi, karena merupakan waktu kompilasi parameter, yang nilai defaultnya mungkin telah diubah oleh Anda (atau siapa pun yang mengkompilasi SQLite).
Ini bermasalah dalam praktiknya, karena tidak jarang aplikasi Anda menyediakan daftar (besar secara sewenang-wenang) ke mesin DB. Misalnya jika Anda ingin massal-DELETE
(atau SELECT
) baris berdasarkan, katakanlah, daftar ID. Pernyataan seperti
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)
Code language: SQL (Structured Query Language) (sql)
akan menimbulkan kesalahan dan tidak akan selesai.
Untuk memperbaikinya, pertimbangkan langkah-langkah berikut:
- Analisis daftar Anda dan bagi menjadi daftar yang lebih kecil,
- Jika diperlukan pemisahan, pastikan untuk menggunakan
BEGIN TRANSACTION
danCOMMIT
untuk meniru atomitas yang dimiliki oleh satu pernyataan . - Pastikan untuk mempertimbangkan juga
?
variabel yang mungkin Anda gunakan dalam kueri Anda yang tidak terkait dengan daftar masuk (mis.?
variabel yang digunakan dalamORDER BY
kondisi), sehingga total jumlah variabel tidak melebihi batas.
Solusi alternatif adalah penggunaan tabel sementara. Idenya adalah membuat tabel sementara, menyisipkan variabel kueri sebagai baris, lalu menggunakan tabel sementara itu dalam subkueri, mis.
DROP TABLE IF EXISTS temp.input_data
CREATE TABLE temp.input_data (some_column TEXT UNIQUE)
# Insert input data, running the next query multiple times
INSERT INTO temp.input_data (some_column) VALUES (...)
# The above DELETE statement now changes to this one:
DELETE FROM some_table WHERE rowid IN (SELECT some_column from temp.input_data)
Code language: SQL (Structured Query Language) (sql)
Waspadalah terhadap afinitas tipe SQLite
Kolom SQLite tidak diketik dengan ketat, dan konversi tidak selalu terjadi seperti yang Anda harapkan. Jenis yang Anda berikan hanyalah petunjuk . SQLite akan sering menyimpan data apa saja ketik aslinya ketik, dan hanya mengonversi data ke jenis kolom jika konversi tidak loss. Misalnya, Anda cukup memasukkan "hello"
string menjadi INTEGER
kolom. SQLite tidak akan mengeluh, atau memperingatkan Anda tentang ketidakcocokan jenis. Sebaliknya, Anda mungkin tidak mengharapkan data yang dikembalikan oleh SELECT
pernyataan dari INTEGER
kolom selalu berupa INTEGER
. Petunjuk jenis ini disebut sebagai "jenis afinitas" dalam bahasa SQLite, lihat di sini. Pastikan untuk mempelajari bagian manual SQLite ini dengan cermat, untuk lebih memahami arti dari tipe kolom yang Anda tentukan saat membuat tabel baru.
Waspadalah terhadap bilangan bulat besar
SQLite mendukung ditandatangani Bilangan bulat 64-bit , yang dapat disimpan, atau melakukan perhitungan dengannya. Dengan kata lain, hanya angka dari -2^63
ke (2^63) - 1
didukung, karena satu bit diperlukan untuk mewakili tanda!
Itu berarti bahwa jika Anda berharap untuk bekerja dengan jumlah yang lebih besar, mis. Bilangan bulat 128-bit (bertanda) atau bilangan bulat 64-bit tidak bertanda, Anda harus konversi data ke teks sebelum memasukkannya .
Kengerian dimulai saat Anda mengabaikan ini dan cukup memasukkan angka yang lebih besar (sebagai bilangan bulat). SQLite tidak akan mengeluh dan menyimpan bulat sebagai gantinya! Misalnya, jika Anda memasukkan 2^63 (yang sudah berada di luar rentang yang didukung), SELECT
nilai ed akan menjadi 9223372036854776000, dan bukan 2^63=9223372036854775808. Bergantung pada bahasa pemrograman dan library binding yang Anda gunakan, perilakunya mungkin berbeda! Misalnya, pengikatan sqlite3 Python memeriksa luapan bilangan bulat seperti itu!
Jangan gunakan REPLACE()
untuk jalur file
Bayangkan Anda menyimpan jalur file relatif atau absolut dalam TEXT
kolom di SQLite, mis. untuk melacak file pada sistem file yang sebenarnya. Berikut adalah contoh dari tiga baris:
foo/test.txt
foo/bar/
foo/bar/x.y
Misalkan Anda ingin mengganti nama direktori "foo" menjadi "xyz". Perintah SQL apa yang akan Anda gunakan? Yang ini?
REPLACE(path_column, old_path, new_path)
Code language: SQL (Structured Query Language) (sql)
Inilah yang saya lakukan, sampai hal-hal aneh mulai terjadi. Masalah dengan REPLACE()
adalah bahwa itu akan menggantikan semua kejadian. Jika ada baris dengan path “foo/bar/foo/”, maka REPLACE(column_name, 'foo/', 'xyz/')
akan mendatangkan malapetaka, karena hasilnya bukan “xyz/bar/foo/”, tetapi “xyz/bar/xyz/”.
Solusi yang lebih baik adalah seperti
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"
Code language: SQL (Structured Query Language) (sql)
4
mencerminkan panjang jalur lama ('foo/' dalam kasus ini). Perhatikan bahwa saya menggunakan GLOB
bukannya LIKE
untuk memperbarui hanya baris yang dimulai dengan 'foo/'.
Kesimpulan
SQLite adalah mesin database yang fantastis, di mana sebagian besar perintah bekerja seperti yang diharapkan. Namun, seluk-beluk tertentu, seperti yang baru saja saya sajikan, masih membutuhkan perhatian pengembang. Selain artikel ini, pastikan Anda juga membaca dokumentasi peringatan SQLite resmi.
Pernahkah Anda menemukan peringatan lain di masa lalu? Jika demikian, beri tahu saya di komentar.