Lulucat

Mengapa Kami Belum Menggunakan Pangkalan Data

Gaoge ZhangGaoge Zhang

Kami menilai Turso/libSQL untuk app tulisan tangan iPad kami, mengukur segala-galanya dan memilih fail snapshot rata. Beban kerja ini tidak memerlukan pangkalan data — dan setiap langkah hanya patut membayar masalah yang sudah wujud.

Lulucat Notes ialah app tulisan tangan untuk iPad. Sehingga minggu lalu, ia hanya mempunyai satu kanvas dan belum mempunyai konsep nota kedua. Kami akan menambah pustaka nota — berbilang dokumen, setiap satunya dengan berbilang halaman — dan soalan seni bina pertama ialah storan.

Pangkalan data nampak seperti jawapan yang jelas. App nota menyimpan data berstruktur. Data berstruktur masuk ke pangkalan data. Kami menilai Turso dan Swift SDK-nya, menjalankannya pada simulator iOS, menanda aras data sapuan sebenar, kemudian memilih untuk tidak menggunakannya.

Sebaliknya, kami memilih fail rata. Inilah yang kami temui dan sebab kami membuat keputusan itu.

Buku nota terbuka dengan nota tulisan tangan dan pen di atas meja kayu.

Foto oleh Gabriel Cox di Unsplash. Lesen Unsplash.

Perkara sebenar yang app lakukan terhadap data

App tulisan tangan mempunyai corak akses data yang sempit dan boleh dijangka. Membaca bermaksud membuka satu halaman dan memuatkan setiap elemennya — semua sapuan, semua imej — ke dalam memori serentak. Kanvas memegang segala-galanya; ia tidak pernah menjalankan pertanyaan separa. Menulis bermaksud menamatkan satu sapuan pen dan menambahkan satu elemen pada halaman. Dalam kes yang jarang berlaku, pengguna memadam sebahagian sapuan, mengalihkan pilihan atau memadam sesuatu, tetapi semuanya masih merupakan operasi satu halaman, satu elemen.

Tiada akses serentak. Seorang pengguna menulis pada satu halaman bagi satu dokumen pada satu masa. Tiada carian merentas dokumen — pustaka nota hanya memerlukan tajuk, cap masa, kiraan halaman dan lakaran kecil muka depan bagi setiap dokumen; tiada satu pun memerlukan kandungan halaman dibaca.

Pertanyaan, indeks dan penyelarasan keserentakan ialah perkara yang dibina untuk pangkalan data. App kami tidak menggunakan satu pun daripada tiga perkara itu.

Penilaian Turso

Kami menilai libsql-swift, Swift SDK rasmi untuk enjin libSQL Turso.

SDK itu berfungsi. Kesemua 9 kes ujiannya lulus. Kami mengintegrasikannya ke dalam salinan app, membinanya untuk simulator iOS, melancarkannya dan mencipta pangkalan data setempat dalam sandbox app. Kami menulis 100 sapuan dengan 3,400 titik pensampelan setiap satu — 4,080,000 bait data BLOB — dalam satu transaksi. Ia mengambil kira-kira 0.019 saat pada Mac pembangunan kami.

Selepas menjalankan PRAGMA wal_checkpoint(TRUNCATE), fail WAL mengecil menjadi sifar dan kami dapat menyalin fail .db utama sahaja ke lokasi lain, membukanya dan membaca semula semua data. Enjin itu sendiri kukuh.

SDK itu mempunyai kos. CLibsql.xcframework berukuran 161 MB. Selepas dipautkan, binaan simulator Debug kami meningkat daripada kira-kira 1.9 MB kepada kira-kira 8.2 MB. API-nya segerak dan menyekat, tanpa pembalut Swift Concurrency. Tiada kaedah close() yang jelas. Transaction.commit() tidak melempar ralat — API C di bawahnya mengembalikan void. README repositori melabelkan SDK itu sebagai “technical preview”, dan komit terkini adalah kira-kira setahun sebelum penilaian kami, pada Julai 2025.

Ekosistem Turso mempunyai jurang. Turso kini mengesyorkan enjin baharu “Turso Database” dan protokol “Turso Sync” untuk projek baharu. Turso Sync mempunyai SDK klien untuk TypeScript, Python, Go dan Rust. Ia tidak mempunyai SDK untuk Swift. Mod Embedded Replica yang lebih lama wujud dalam libsql-swift, tetapi initializer Swift-nya tidak mendedahkan parameter offline yang diperlukan oleh app mudah alih local-first sepenuhnya. Mengguna pakai libsql-swift hari ini memberi kami fork SQLite setempat, tetapi bukan keupayaan penyegerakan yang menjadikan Turso berbeza.

Kos pangkalan data kepada kami sekarang

Walaupun SDK itu matang, kami masih akan membayar kos yang tidak memberi apa-apa kepada beban kerja kami:

Pengurusan fail sampingan WAL. Pangkalan data yang sedang berjalan mencipta fail teman -wal dan -shm. Menyalin dokumen bermaksud melakukan checkpoint terlebih dahulu atau menyalin ketiga-tiga fail secara atomik. Mengeksport pakej .lnote ke Files atau AirDrop kini memerlukan langkah pra-eksport yang tidak dapat dilihat pengguna dan tidak boleh dilupakan pembangun.

Lapisan penyesuai. Sapuan perlu disiri menjadi BLOB dan dinyahsirikan semula. Elemen halaman mempunyai susunan tatasusunan semula jadi yang boleh dirender terus oleh kanvas; pangkalan data akan memperkenalkan susunan baris dan lajur z-index. Kami akan menulis lapisan terjemahan antara dua perwakilan data yang sama dan menyelenggarakannya pada setiap perubahan skema.

Kebergantungan 161 MB. Untuk app yang binaan Debug-nya di bawah 2 MB, kebergantungan yang lebih besar daripada 80× app itu sendiri ialah kos yang wajar diberi perhatian — terutamanya jika ia dilabel “technical preview” dan tidak aktif selama setahun.

Kos ini bukan andaian. Ia bermula sebaik sahaja kebergantungan dipautkan. Dan ia membeli keupayaan — pertanyaan, pengindeksan, penulisan serentak — yang tidak digunakan oleh app kami.

Penyelesaian yang kami keluarkan: pakej fail snapshot

Dokumen .lnote ialah pakej direktori:

Documents/Notes/<UUID>.lnote/
  manifest.json              # library cache: title, time, page count, cover
  document.json              # source of truth: document metadata + page order
  pages/
    <page-uuid>.content      # one snapshot per page
  assets/                    # document-level shared resources
    <asset-uuid>.jpg
  thumbnails/
    <page-uuid>.jpg          # per-page thumbnail; first page doubles as cover

document.json ialah sumber kebenaran bagi struktur dokumen: ID, tajuk, cap masa dan senarai halaman tersusun yang mengandungi saiz kanvas, cap masa serta kiraan elemen setiap halaman. Fail kandungan halaman menyimpan tatasusunan elemen dalam pengekodan integer terkuantum yang sudah digunakan oleh app — koordinat dan jejari pada ketepatan 0.1 titik, tekanan dalam perseribu, cap masa dalam milisaat relatif.

Pustaka nota hanya membaca manifest.json dan lakaran kecil muka depan. Ia tidak pernah menghuraikan document.json atau mana-mana kandungan halaman. Membuka halaman memuatkan satu fail .content. Itulah satu-satunya pembacaan fail yang menyentuh data sapuan.

Halaman menyelesaikan write amplification

App tulisan tangan sudah mempunyai konsep halaman — itulah unit yang difikirkan pengguna, perkara yang mereka leret untuk bertukar. Menjadikan halaman sebagai unit pengekalan bermakna auto-simpan hanya menulis semula halaman yang berubah.

Satu halaman tulisan tangan — katakan 1,000 hingga 2,000 sapuan — menggunakan kira-kira 3 hingga 5 MB dalam format terkuantum kami. Satu rakaman 21 sapuan dengan 3,400 titik pensampelan dikuantumkan kepada kira-kira 55 KB. Menulis satu snapshot halaman ke storan flash mengambil masa 10 hingga 20 milisaat pada perkakasan moden. Dengan debounce 0.5 saat, penyimpanan tidak kelihatan kepada pengguna.

Kos penyimpanan berkembang mengikut jumlah tulisan pada halaman semasa, bukan jumlah keseluruhan halaman dalam dokumen. Buku nota 200 halaman disimpan tepat sepantas buku nota 2 halaman, kerana hanya halaman yang kotor ditulis semula.

Setiap penulisan menggunakan operasi fail atomik — tulis ke fail sementara, kemudian namakan semula — jadi ranap semasa penyimpanan tidak dapat menghasilkan halaman yang terpotong. Apabila masuk ke latar belakang, app melakukan flush pada semua halaman kotor dengan segera, sepadan dengan tingkah laku yang sudah ada ketika app hanya mempunyai satu kanvas.

Konsistensi tanpa transaksi

Pakej fail tidak mempunyai transaksi, tetapi mempunyai peraturan pemilikan yang jelas dan mencapai tujuan yang sama:

Sumber sebelum rujukan. Apabila pengguna menyisipkan imej, fail aset ditulis terus ke assets/. Snapshot halaman yang merujuk aset mengikut ID ditulis kemudian oleh auto-simpan yang dinyah-debounce. Tiada masa halaman merujuk aset yang tidak wujud pada cakera.

Sumber kebenaran menang. document.json dan direktori pages/ ialah sumber kebenaran. manifest.json ialah cache. Jika kedua-duanya tidak sepadan, simpanan seterusnya menyelaraskan cache agar sepadan dengan sumber. Lakaran kecil ialah data terbitan dan boleh dijana semula pada bila-bila masa.

Aset yatim lebih baik daripada rujukan tergantung. Hasil paling buruk daripada ranap ialah aset yatim — fail dalam assets/ yang tidak dirujuk oleh mana-mana halaman. Aset yatim dibersihkan apabila dokumen ditutup. Keadaan sebaliknya — halaman merujuk fail yang hilang — tidak boleh berlaku, kerana aset ditulis sebelum snapshot halaman yang merujuknya.

Peraturan ini lebih mudah difahami berbanding checkpointing WAL dan pengasingan transaksi, serta sepadan tepat dengan corak akses satu proses, satu halaman app.

Laluan naik taraf telah ditulis

Memilih fail rata sekarang tidak bermakna memilih fail rata selama-lamanya. Struktur pakej direka supaya naik taraf enjin storan mengubah kandungan di dalam pakej tanpa mengubah pakej itu sendiri.

Tahap 1: snapshot + jurnal append. Jika write amplification suatu hari nanti menjadi ketara — misalnya, penulisan berterusan pada halaman dengan ribuan sapuan menyebabkan kelewatan simpanan yang dapat dilihat — setiap fail halaman dipecahkan menjadi snapshot dan jurnal append-only. Elemen baharu ditambah sebagai frame [length][CRC][type][payload]. Semasa memainkan semula jurnal, mana-mana frame yang CRC-nya tidak sepadan dibuang, lalu menyediakan keselamatan daripada ranap. Apabila jurnal melebihi ambang atau halaman ditutup, ia digabungkan semula ke dalam snapshot. Ini kira-kira 200 baris kod dengan sifar kebergantungan luaran.

Oleh sebab snapshot peringkat halaman sudah menghapuskan write amplification merentas halaman, tahap ini mungkin tidak diperlukan untuk masa yang lama. Menulis semula halaman 5 MB setiap 0.5 saat masih jauh dalam belanjawan penulisan flash.

Tahap 2: pangkalan data SQLite. Jika app memerlukan carian teks penuh merentas nota, penyegerakan setiap elemen atau pengindeksan merentas dokumen, SQLite menjadi alat yang tepat. Enjin yang berkemungkinan ketika itu ialah GRDB, pembalut Swift matang yang dikompil daripada sumber dengan overhed saiz binari hampir sifar. libsql-swift hanya akan dipertimbangkan semula jika ekosistem Turso — khususnya Turso Sync untuk Swift — menjadi keperluan produk yang sebenar.

Laluan penghijrahan bersifat mekanikal: tatasusunan elemen setiap halaman dipetakan kepada jadual strokes / images, dengan satu BLOB tidak berubah bagi setiap sapuan (16 bait bagi setiap titik pensampelan dalam binari little-endian). Penanda aras 0.019 saat untuk 100 sapuan mengesahkan bahawa pendekatan ini boleh dilaksanakan. Sebelum dikeluarkan, penghijrahan akan mengesahkan bahawa kiraan elemen dan kiraan aset sepadan antara pakej lama dan pangkalan data baharu, serta pemulihan ranap, had WAL dan flush latar belakang semuanya lulus.

Bila hendak membayar

Keputusan ini bukan penghakiman terhadap pangkalan data. SQLite mengendalikan penulis serentak, pertanyaan kompleks dan pemulihan ranap merentas keadaan dikongsi — tiada satu pun diperlukan oleh app kami sekarang. Membayar keupayaan tersebut sebelum app mempunyai masalah yang diselesaikannya ialah kerugian bersih.

Kos pangkalan data — kebergantungan, pengurusan WAL, lapisan penyesuai, saiz binari — bermula sebaik sahaja pustaka dipautkan. Manfaat bermula apabila app mempunyai pertanyaan untuk dijalankan, indeks untuk diselenggara atau penulis serentak untuk diselaraskan. Pada peringkat ini, app kami tidak mempunyai satu pun daripada tiga perkara itu.

Setiap langkah dalam evolusi storan kami hanya akan membayar masalah yang sudah muncul. Snapshot peringkat halaman membayar masalah yang kami ada hari ini: menyimpan dokumen berbilang halaman tanpa menulis semula keseluruhan fail. Jika write amplification menjadi boleh diukur, jurnal append akan membayar masalah itu. Jika carian atau penyegerakan menjadi keperluan produk, pangkalan data akan membayar masalah itu.

Struktur pakej, manifest dan skema dokumen tidak terikat pada mana-mana enjin storan. Kos pertukaran rendah kerana sempadannya berada di tempat yang betul. Apabila tiba masanya kami benar-benar memerlukan pangkalan data, kami akan menggunakannya untuk masalah khusus yang sudah diukur — bukan masalah hipotesis.