- Dengan menambahkan
STRICT di akhir definisi tabel, kesalahan tipe seperti teks sembarang masuk ke kolom bilangan bulat dapat dicegah lebih awal, sehingga meningkatkan integritas data
- Saat penyisipan dan pembaruan, tipe diperiksa, tetapi nilai yang dapat dikonversi tanpa kehilangan seperti
'123' tetap diizinkan; tipe kolom dibatasi pada INT, INTEGER, REAL, TEXT, BLOB, ANY
- Untuk kolom yang perlu menampung beberapa tipe,
ANY dapat ditentukan, sehingga validasi ketat dan penyimpanan fleksibel bisa diterapkan bersama dalam satu tabel
- Tabel yang sudah ada tidak bisa langsung diubah menjadi STRICT, sehingga diperlukan pembuatan tabel baru dan penyalinan data; data lama yang salah harus dibersihkan atau dikonversi tipenya
- Tabel STRICT hanya didukung pada SQLite 3.37.0 ke atas; meskipun secara teori ada biaya pemeriksaan, eksperimen tidak resmi tidak menemukan perbedaan performa atau ukuran file yang jelas
Membuat tabel STRICT
- Tabel STRICT di SQLite menerapkan pemeriksaan tipe yang lebih ketat, mirip dengan mesin SQL lain
- Cukup tambahkan
STRICT di akhir definisi CREATE TABLE
CREATE TABLE people (name TEXT) STRICT;
Pemeriksaan tipe pada penyisipan dan pembaruan
- Tabel SQLite biasa dapat menyimpan teks seperti
'garbage' bahkan di kolom INTEGER, tetapi tabel STRICT memperlakukan ketidakcocokan tipe sebagai kesalahan
CREATE TABLE people_nonstrict (age INTEGER);
INSERT INTO people_nonstrict (age) VALUES ('garbage');
-- diproses normal
CREATE TABLE people_strict (age INTEGER) STRICT;
INSERT INTO people_strict (age) VALUES ('garbage');
-- kesalahan: cannot store TEXT value in INTEGER column
- Validasi yang sama juga diterapkan pada
UPDATE, sehingga tipe yang salah tetap dicegah saat nilai diubah setelah disimpan
- Namun, jika nilai dapat dikonversi tanpa kehilangan, tabel STRICT tetap mengizinkannya
- String
'123' dapat dikonversi sepenuhnya menjadi bilangan bulat 123, sehingga dua penyisipan di bawah ini diproses secara sama
INSERT INTO people_strict (age) VALUES ('123');
INSERT INTO people_strict (age) VALUES (123);
Batasan tipe pada tahap definisi tabel
- Pada tabel biasa, nama tipe yang tidak didukung SQLite pun dapat digunakan dalam deklarasi kolom
GARBAGE, DATETIME, JSON, UUID, BLOBB semuanya diterima
- Deklarasi seperti ini bisa berasal dari salah ketik atau kesalahpahaman tentang tipe data yang didukung SQLite
- Tabel STRICT akan menghasilkan kesalahan pada tahap pembuatan jika nama tipe yang tidak didukung digunakan
CREATE TABLE tbl (name GARBAGE) STRICT;
CREATE TABLE tbl (name DATETIME) STRICT;
CREATE TABLE tbl (name JSON) STRICT;
CREATE TABLE tbl (name UUID) STRICT;
CREATE TABLE tbl (name BLOBB) STRICT;
- Tipe kolom yang diizinkan hanya
INT, INTEGER, REAL, TEXT, BLOB, ANY
- Semua kolom harus memiliki tipe, sehingga tipe tidak bisa dihilangkan seperti
CREATE TABLE tbl (name)
Menjaga fleksibilitas dengan ANY
- Jika perlu menyimpan data dengan tipe yang tidak tetap, kolom
ANY dapat digunakan
- Di dalam tabel STRICT sekalipun, kolom
ANY dapat menyimpan semua tipe seperti bilangan bulat, teks, bilangan real, dan BLOB
CREATE TABLE tbl (value ANY) STRICT;
INSERT INTO tbl (value) VALUES (123);
INSERT INTO tbl (value) VALUES ('text');
INSERT INTO tbl (value) VALUES (12.34);
INSERT INTO tbl (value) VALUES (X'8647');
Migrasi tabel yang sudah ada
- Tidak ada cara untuk langsung mengubah tabel non-strict yang sudah ada menjadi tabel STRICT dengan
ALTER, sehingga lebih sederhana jika membuatnya ketat sejak awal
- Untuk mengonversi tabel yang sudah ada, perlu melalui prosedur membuat tabel STRICT baru → menyalin data → mengganti tabel lama
CREATE TABLE new_people (name TEXT) STRICT;
INSERT INTO new_people SELECT * FROM people;
DROP TABLE people;
ALTER TABLE new_people RENAME TO people;
- Jika data lama memiliki tipe yang salah, misalnya teks di kolom bilangan bulat, proses penyalinan dapat menghasilkan kesalahan
- Sebelum migrasi, data mungkin perlu dibersihkan atau dikonversi tipenya dengan
CAST
- Kebijakan menerapkan STRICT hanya pada tabel baru juga memungkinkan, tetapi tingkat validasi akan berbeda antar tabel, sehingga perilakunya bisa lebih sulit diprediksi dibandingkan jika semua tabel longgar
Kapan tipe fleksibel cocok digunakan
- Tim pengembang SQLite secara terpisah merangkum keunggulan tipe fleksibel dan memberikan contoh kasus ketika perilaku default berguna
- Kegunaan yang mungkin masuk akal untuk tabel non-strict antara lain:
- Penyimpanan key-value murni yang menampung berbagai tipe apa adanya
- Ruang untuk menyimpan atribut tambahan dengan tipe yang berbeda-beda
- Kasus ketika mengimpor CSV yang berantakan secara langsung dan harus mempertahankan nilai yang tidak valid tanpa kehilangan
- Namun, tipe yang tidak terduga dapat menyebabkan bug yang halus, sehingga pada tabel umum lebih menguntungkan untuk langsung menghasilkan kesalahan daripada mengizinkannya secara diam-diam
- Di kode sumber SQLite juga ada komentar yang menyebut tabel non-strict sebagai
legacy, tetapi ini sulit dijadikan dasar yang lebih tepercaya daripada dokumentasi resmi
Kompatibilitas versi
- Tabel STRICT diperkenalkan di SQLite 3.37.0 yang dirilis pada November 2021
- Versi SQLite sebelumnya tidak dapat menggunakan tabel STRICT
- Versi lama juga tidak dapat membaca database yang berisi tabel STRICT
- Jika tabel STRICT dibuat dengan SQLite terbaru lalu database yang sama dibuka dengan SQLite 3.36.0, akan terjadi kesalahan
Performa dan ruang penyimpanan
- Karena tabel STRICT memeriksa tipe data tambahan saat penyisipan dan pembaruan, secara teori tabel ini bisa lebih lambat
- Dalam eksperimen tidak resmi yang menyisipkan jutaan baris ke tabel dengan 100 kolom, tidak terlihat perbedaan performa yang jelas di berbagai komputer
- Ukuran file database di disk juga sama, tetapi karena ini bukan benchmark menyeluruh, mungkin ada perbedaan yang tidak terdeteksi
- Ada kemungkinan performa justru membaik karena nilai yang tidak sesuai dengan afinitas kolom (column affinity) dicegah agar tidak tersimpan secara tidak sengaja, tetapi ini belum diverifikasi secara terpisah
Hal yang perlu dipertimbangkan saat menerapkan
- Tabel STRICT tidak menyelesaikan semua masalah data, tetapi mengurangi kesalahan terkait tipe dan memperkuat integritas data
- Dalam kebanyakan kasus, penerapannya sederhana karena cukup menambahkan
STRICT ke definisi tabel
- Pada tabel umum, manfaat dari validasi tipe yang ketat lebih besar daripada beban migrasi, ketidakcocokan dengan versi lama, dan fleksibilitas yang terbatas
1 komentar
Opini Hacker News
Berangkat dari fakta bahwa SQLite tidak memiliki fitur untuk
ALTERtabel agar berubah ke mode ketat, sehingga data dari tabel non-ketat harus disalin ke tabel ketat, sqlite-utils 4.1 menambahkan fitur konversi dua arahDi CLI bisa digunakan dengan
uvx sqlite-utils transform data.db mytable --strict, sedangkan di Python dengandb.table("mytable").transform(strict=True)Catatan rilis dapat dilihat di https://sqlite-utils.datasette.io/en/stable/changelog.html#v..., dokumentasi Python API di https://sqlite-utils.datasette.io/en/stable/python-api.html#..., dan dokumentasi CLI di https://sqlite-utils.datasette.io/en/stable/cli.html#transfo...
https://sqlite.org/flextypegood.html menjelaskan mengapa penentuan tipe yang fleksibel menjadi default dan kemungkinan tidak akan berubah ke depannya, tetapi itu tidak sejalan dengan pengalaman nyata
Berbeda dengan penjelasan bahwa memasukkan nama pelanggan ke integer
Customer.creditScorepun dapat dengan mudah ditemukan dan diperbaiki, baris yang rusak sulit dipulihkan dan datanya bisa saja sudah hilang sepenuhnyaJuga sangat sulit menyetujui logika bahwa pemeriksaan tipe yang ketat hanya mencegah kesalahan yang mudah ditemukan, sehingga justru mempersulit deteksi dan perbaikan bug
Di Postgres, biasanya sebanyak mungkin pemeriksaan dan pengaman ditambahkan, sehingga kita tidak perlu mencari-cari belakangan kesalahan yang sejak awal semestinya tidak terjadi
Ini tampak lebih seperti hasil dari era ketika SQLite dibuat dan keyakinan kuat penulisnya, bukan prinsip rekayasa yang ketat; kesannya, setelah banyak dikritik, posisinya tetap dipertahankan dengan alasan apa pun yang bisa digunakan
Tipe seperti JSON atau HSTORE di Postgres dapat menyediakan fleksibilitas yang diinginkan, tetapi hampir selalu lebih baik menjadikannya pilihan saat diperlukan, bukan memaksakan penentuan tipe tanpa batas sebagai default
Akan lebih baik jika STRICT menjadi default
Selain poin ini, pengembang SQLite adalah orang hebat yang membuat alat yang luar biasa
PRAGMA foreign_keys = ON;Masalah yang lebih besar adalah tidak ada pragma yang setara untuk tabel ketat, sehingga setiap
CREATE TABLEharus diberiSTRICTyang non-standarPragma STRICT global juga pernah dipertimbangkan tetapi tidak diimplementasikan; materi terkait dapat dilihat di https://sqlite.org/foreignkeys.html dan https://sqlite.org/forum/forumpost/1b9d073a37ca5998
Tujuannya agar perangkat lunak untuk SQLite 3.53, setelah ditingkatkan ke 3.54, tidak tiba-tiba membuat tabel ketat saat
CREATE TABLE, lalu memunculkan error dan merusak semuanyaMembuat penasaran apakah dulu SQLite hanya mendukung sintaks foreign key tanpa mengimplementasikan fungsi sebenarnya
Pernah ada kode yang diterapkan ke ribuan perangkat yang menyimpan string
'1'dan'0'di kolom boolean, lalu harus dibersihkan; itu sama sekali tidak menyenangkanTipe timestamp juga tidak ada sehingga harus disimpan di kolom teks, dan bahkan fungsi tanggal/waktu standar menghasilkan
yyyy-mm-dd HH:MM:SSyang harus diasumsikan secara implisit sebagai UTC, alih-alih format ISOyyyy-mm-ddTHH:MM:SSZSQLite benar-benar proyek yang luar biasa, tetapi beberapa keputusan desainnya membingungkan
Dari sudut pandang yang datang dari lingkungan SQL enterprise, saya tidak menganggap SQLite secara serius karena tipe field pada dasarnya tidak dipaksakan, dan saya juga terkejut ketika SQLite menjadi dasar metadata aplikasi smartphone
Ini mengingatkan pada kisah lama jaringan: memilih UDP karena latensi rendah dan kesederhanaannya, lalu membuat ulang sendiri sebagian besar fitur keandalan TCP di tingkat aplikasi
Namun mengimplementasikan sendiri pemeriksaan tipe di atas SQLite yang tidak ketat tampaknya tidak memberi kekuatan serupa
PostgreSQL pun default-nya ditujukan untuk sistem berspesifikasi rendah, sehingga perlu konfigurasi untuk mendapatkan performa optimal
Selain itu, karena TCP berbasis stream, banyak aplikasi mengalami masalah HOL blocking, dan jika Anda membuat sendiri lapisan keandalan di atas UDP, setelah banyak pekerjaan pun Anda bisa mendapatkan hasil yang lebih baik daripada TCP
Mereka menilainya berisik dan buruk pengendaliannya, karena mereka mendekati produk yang sejak awal ditujukan untuk kegunaan lain seolah-olah hanya use case mereka sendiri yang penting
Tabel strict memang punya kekurangan, yaitu tidak bisa memakai beberapa tipe seperti Date, tetapi tetap seharusnya menjadi default
Jika beberapa aplikasi berbagi database, tipe yang dideklarasikan harus bisa dipercaya; jika salah satunya menyimpan string di kolom angka, semuanya akan rusak
Sebaliknya, embedded database, yang merupakan penggunaan utama SQLite, biasanya hanya dipakai oleh satu aplikasi, sehingga ada juga keuntungan dapat mengembangkan skema alih-alih membuat database baru dan menyalinnya
Dalam hal ini, kode aplikasi tahu apa yang harus diharapkan dari tiap kolom, termasuk tipe campuran
INTEGER,TEXT,BLOB,REAL,NUMERIC: https://sqlite.org/datatype3.htmlEXPLAINpada querySELECTsembarang untuk mendapatkan nama tipe yang secara formal ditentukanKarena itu, meski memakai nama seperti
DATEatauDATETIME, bahkan itu pun tidak bisa diinferensikanDatebukan tipe nyata; yang dibuat hanyalah kolom dengan numeric affinityMode strict membatasi notasi tipe kolom sehingga tidak bisa memakai nama yang lebih bermakna, dan membuat kode tidak bisa memanfaatkan nama itu saat memetakan tipe database ke tipe aplikasi; akibatnya justru menghambat sistem tipe yang lebih ketat di lapisan aplikasi
Detailnya ada di https://hn.algolia.com/?query=chrismorgan+strict+sqlite&type..., dan jika menangani database dengan crate seperti sqlx di Rust, saya rasa lebih baik menghindari mode strict
Untuk database dan RPC, saya menginginkan tipe yang ketat, tetapi SQLite memiliki kegunaan yang agak berbeda; setelah mencobanya langsung, mungkin Anda akan lebih memahami https://sqlite.org/flextypegood.html
Misalnya, tujuan agar skrip sembarang yang bukan untuk SQLite pun secara kebetulan dapat berjalan adalah karakteristik yang biasanya tidak dipertimbangkan di DBMS lain
Keunggulan utama penentuan tipe yang fleksibel adalah mudah mengembangkan skema
Setiap kali kebutuhan embedded database berubah, alih-alih membuat database baru dan memigrasikan data, Anda bisa mengubah skema di tempat; dan jika hanya satu aplikasi yang membaca dan menulis, tidak mengejutkan bila string ditambahkan ke kolom integer
Sebaliknya, jika beberapa aplikasi diperbarui sesuai jadwal masing-masing dan berbagi database yang sama, skema adalah kontrak, sehingga satu tipe yang salah dapat merusak aplikasi lain
Jika tabel yang dibuat oleh satu aplikasi digunakan oleh aplikasi lain, tipe data yang disepakati harus dipatuhi secara ketat
Memeriksanya lagi saat runtime tidak memberi keuntungan nyata, dan kode yang melalui analisis statis dipastikan memasukkan string tidak akan tiba-tiba bermutasi lalu memasukkan integer
SQLite berbeda dari Postgres, tempat beberapa aplikasi berbagi data dan harus memercayai pihak ketiga
Di lingkungan seperti Postgres, validasi runtime wajib, tetapi SQLite pada dasarnya dirancang untuk satu aplikasi dan satu database, sehingga cukup memercayai kode sendiri yang bisa dievaluasi pada waktu kompilasi
Dalam situasi tidak lazim ketika beberapa aplikasi berbagi satu file, tabel strict dapat diaktifkan
Tipe data dinamis mungkin cocok untuk penggunaan seperti penyimpanan key-value sederhana, tetapi perlu ditanyakan mana yang sesuai dengan principle of least surprise:
INTEGERmenerima'hello world'tanpa error, atau nilai semacam itu hanya bisa dimasukkan jika harus secara eksplisit memakai keyword sepertiNONSTRICTatau tipeANYSebagian besar pengguna SQLite tampaknya tidak akan menduga bahwa yang pertama mungkin terjadi
Ini juga sejalan dengan penjelasan bahwa mereka berharap kode untuk DBMS lain kebetulan dapat berjalan di SQLite
Namun tetap saja sangat mengejutkan bahwa pengguna tidak menghilangkan tipe, melainkan secara eksplisit menulis
INTEGER, lalu masih bisa memasukkan teksPernah ada kasus UUID yang dimulai dengan nilai seperti
08123…diparse sebagai oktal, sehingga sebagian keliru dikonversi menjadi angkaItu membingungkan dan menyebalkan, tetapi terselesaikan dengan menerapkan
STRICTdan membuat ulang seluruh tabelSQLite tidak memiliki tipe oktal
Gunakan
CREATE TABLE ... STRICT WITHOUT ROWIDsebagai default, dan saya tidak melihat alasan untuk membuatnya berbeda