· 8 menit baca

Bagaimana kami membangun engine SQL lokal untuk DynamoDB (sebuah database tanpa JOIN)

DynamoDB bisa mengekspor tabel 100 GB tetapi tak bisa mengagregasinya. Tidak ada JOIN, tidak ada GROUP BY, tidak ada COUNT(*) — bukan versi lambatnya, tidak sama sekali — dan bahkan , permukaan ala-SQL milik AWS sendiri, tidak menambahkannya. Jadi setiap tim akhirnya menulis skrip sekali-pakai yang sama: memaginasi sebuah , melipat baris ke dalam sebuah map, mencetak jawabannya, menghapus skrip itu.

Workbench milik DynoTable adalah jawaban kami untuk skrip itu: sebuah tab di mana Anda menulis satu SELECT sungguhan — JOIN … ON multi-tabel, WHERE, GROUP BY, HAVING, DISTINCT, CASE, agregat — terhadap tabel yang tidak mendukung satu pun darinya. Postingan ini adalah bagaimana ia bekerja di balik layar, parser yang diam-diam membohongi kami, dan mengapa kami kemudian mencabut model memori engine tersebut.

Jangan membangun engine query

Keputusan inti adalah menolak menulis eksekutor relasional. Halaman-halaman DynamoDB mengalir ke dalam database SQLite tertanam, dan SQLite — salah satu engine query yang paling banyak diuji di bumi — melakukan pekerjaan relasionalnya:

which tables,which join keysrewritten SQLYour SQLCompilersql-parser-cstDynamoDBpage streamLocal SQLiteon diskResult grid

Setiap item DynamoDB mendarat di SQLite sebagai amplop bertipe mentahnya ({platform: {S: "ios"}}) dalam satu kolom item. Sebuah user-defined function kecil membuka bungkus atribut saat query, sehingga SELECT platform Anda dikompilasi menjadi panggilan attr(item, 'platform', …). Satu detail dalam fungsi itu menahan beban: ia membungkus penguraian JSON-nya dalam sebuah catch, karena binding SQLite mengubah error penguraian satu baris yang cacat menjadi pembatalan seluruh prepared statement. Tanpa catch itu, satu baris yang rusak akan mematikan query.

Batasan jujur yang kami pertahankan: fisika pola-akses DynamoDB tetap berlaku. Sisi-tujuan sebuah JOIN harus berupa primary key atau partition key — engine menyelesaikan join dengan pencarian key terhadap stream, tak pernah dengan scan cross-product. Jika Anda membutuhkan join sembarang pada atribut non-key, itu adalah percakapan pemodelan, bukan fitur engine query.

Workbench: sebuah JOIN SQL multi-tabel, dengan grid hasil gabungan di bawahnya.
Workbench: sebuah JOIN SQL multi-tabel, dengan grid hasil gabungan di bawahnya.

Parser yang membohongi kami

Versi pertama menggunakan parser SQL siap-pakai yang populer. Selama beberapa minggu, empat bug ketepatan independen dilacak kembali ke sana:

  • SELECT DISTINCT diam-diam dibuang. Parser menerimanya dan emitter mengabaikannya — setiap baris duplikat kembali. Laporan bug seorang pengguna berbunyi: "menampilkan tabel semua nilai, bukan penghitung."
  • Apa pun yang tak tertangani menjadi NULL SQL literal. CASE, CAST, subquery skalar — fallback emitter untuk node sintaks yang tak dikenal adalah return 'NULL'. Query berjalan, mengembalikan jawaban salah yang penuh percaya diri, dan tidak memunculkan error.
  • Kapitalisasi identifier tidak berperilaku seperti SQL. SELECT PLATFORM FROM t mengembalikan null ketika atributnya adalah platform — kebalikan dari apa yang dilatihkan setiap database SQL untuk Anda harapkan.
  • Autocomplete tidak sepakat dengan resolver, menyarankan nama yang kemudian gagal diselesaikan oleh compiler.

Penggantinya, sql-parser-cst, menang pada satu primitif yang tak ditemukan survei di tempat lain mana pun: pohonnya mempertahankan baik teks mentah maupun nama ternormalisasi dari setiap identifier — sehingga compiler bisa membedakan platform dari "PLATFORM". Satu pembedaan itu memungkinkan satu tata bahasa membawa semantik ANSI yang benar: identifier tak-berkutip diselesaikan secara case-insensitive, yang berkutip adalah escape hatch case-sensitive, persis seperti Postgres atau SQLite. Ia juga membawa source range pada setiap node, sehingga diagnostik menggarisbawahi konstruksi yang bermasalah alih-alih seluruh statement.

Dan kami mengganti fallback return 'NULL' dengan aturan yang kini kami perlakukan sebagai kebijakan: tidak ada NULL diam-diam, tak pernah. Cabang default emitter adalah error dengan rentang yang presisi. Window function mendapat pesan yang menjelaskan mengapa, bukan sekadar tidak:

Window functions are not supported in Workbench.
They have undefined semantics on the joined-row materialization.

Kompromi yang kami terima: sebuah dependensi pra-1.0, dipin ke versi yang tepat, di-upgrade hanya dengan tangan dengan test suite sebagai gerbangnya. Kami mengevaluasi mem-patch parser lama (ia membutuhkan pre-tokenizer paralel — menguraikan setiap masukan dua kali), menggunakan pohon sintaks editor (tingkat-token, tanpa struktur klausa), dan membuatnya sendiri (sebagian besar sebuah parser SQLite, pemeliharaan tak terbatas). Kegagalan ketepatan empat-arah mengalahkan semua kekhawatiran itu.

Mengetahui apa yang tidak boleh diblokir

Satu diagnostik sengaja tidak menghentikan Anda. Ketika sebuah filter merujuk atribut yang kapitalisasinya tak bisa dikonfirmasi schema, kami tak bisa tahu case kanoniknya — filter sisi-server DynamoDB bersifat case-sensitive dan schema-nya hanya mendeklarasikan atribut key. Itu memunculkan peringatan, dan peringatan tak pernah menonaktifkan Run. Kami mempelajari pelajaran kelas ini dengan cara yang sulit dalam kerja validator kami: sebuah gerbang yang menolak bentuk query sah yang paling umum lebih buruk daripada tanpa gerbang.

Batasnya adalah bentuk yang salah

Engine agregasi pertama menyangga baris yang di-scan ke dalam SQLite dalam-memori dengan batas ukuran — 64 MB di sini, 250 MB di sana, dan satu jalur tanpa batas yang bisa membuat aplikasi OOM pada tabel besar. Menyentuh batas itu dan Anda mendapat jawaban parsial dengan flag overflow.

Perancangan ulang dimulai dengan sebuah pembingkaian ulang: pesaingnya adalah skrip sekali-pakai yang justru akan ditulis seorang developer. Dan skrip yang kompeten tidak menyangga-lalu-membatasi — ia mengalirkan, melipat setiap halaman ke dalam total berjalan dengan memori terbatas. Batas dalam-memori itu tidak terlalu kecil; ia adalah bentuk yang salah. Lebih buruk lagi, agregat parsial bukanlah "tidak lengkap" — ia salah. Momen yang membuat ini konkret: demo AI kami sendiri dengan percaya diri menarasikan peringkat "top spenders" parsial sebagai fakta.

Engine Compute yang dibangun ulang bekerja seperti cara skrip bekerja, ditambah segala hal yang tak pernah dipedulikan skrip:

  • Sebuah planner mengklasifikasikan setiap query ke dalam salah satu dari tiga jalur: stream-fold (agregat aljabar biasa — COUNT/SUM/AVG/MIN/MAX — dilipat halaman-demi-halaman di luar proses utama; ukuran tak pernah menggerbang query yang bisa dilipat, ia hanya butuh lebih lama), materialize (pengurutan, join berbatas-key — ditumpahkan ke SQLite berbasis disk, tanpa batas memori), atau penolakan jujur yang merekomendasikan tool kelas-berat yang tepat (ekspor S3 DynamoDB plus Athena) untuk ekor yang tak bisa kami layani.
  • Klasifikasi fold sengaja dibuat sempit. Sebuah HAVING, sebuah ORDER BY, sebuah kolom telanjang di samping agregat — salah satunya memaksa jalur materialize. Sebuah fold yang mengabaikan sebuah klausa dan tetap melaporkan "eksak" akan persis menjadi bug salah-tapi-percaya-diri yang keberadaan planner ini justru untuk mencegahnya.
  • Ketepatan adalah bagian dari hasil. Kolom agregat menampilkan badge partial selama halaman masih mengalir, dan membuangnya hanya ketika scan telah habis. Bahkan kejujuran aritmetika dilacak: jumlah bilangan bulat tetap eksak melewati 2⁵³ (disimpan sebagai integer 64-bit), sementara jumlah pecahan yang melampaui mantissa float menandai dirinya partial: precision alih-alih membulatkan diam-diam.
  • Sekring menggantikan popup untuk mesin. Aplikasi interaktif bertanya sebelum scan lima-juta-item; asisten AI dan pemanggil MCP justru mendapat anggaran scan dan pembatalan batas-halaman. Dan amplop hasil yang dilihat model sengaja dipangkas — kelas biaya dan estimasi ukuran ditahan, karena menampilkan tebakan basi mengundang model untuk menarasikannya sebagai fakta.

Satu catatan biaya yang mengejutkan orang secara internal: untuk agregat sekali-pakai, sebuah scan live kira-kira 6× lebih murah daripada menghasilkan ekspor DynamoDB→S3 — itulah mengapa jalur berbentuk-skrip adalah default dan gudang data adalah rekomendasi untuk analitik berat berulang, bukan refleks. (Kalkulator harga akan menghitung harga satu lintasan penuh atas tabel Anda sendiri.)

Amankah mengarahkan SQL ke produksi?

Workbench bersifat read-only secara struktural. SQL akhir yang mencapai engine lokal melewati sebuah gerbang defense-in-depth yang mentokenisasi dan menolak apa pun yang bukan satu SELECT — sebagai pelanggaran kontrak, bukan kesalahan pengguna, karena compiler seharusnya tak pernah menghasilkannya. Substrat SQLite berjalan dengan built-in berbahaya dinonaktifkan, dan penulisan ke DynamoDB memiliki tepat satu jalur di seluruh aplikasi: staging area yang dapat ditinjau, yang tak bisa dijangkau SQL.

Apa yang dapat dialihkan jika Anda membangunnya

  • Jangan menulis engine query; tulislah compiler yang setia ke atas yang sudah ada. Bug Anda akan tinggal di sambungan (kapitalisasi identifier, amplop tipe, baris rusak), bukan di algoritma join.
  • Tuntut provenans-kutip dari parser Anda. Jika ia tak bisa membedakan name dari "NAME", Anda tak bisa mengimplementasikan semantik case SQL, dan pengguna Anda akan mengetahuinya sebelum Anda.
  • Jadikan "sintaks tak tertangani" sebagai error yang lantang, tak pernah sebuah nilai default. NULL diam-diam adalah cara jawaban salah yang percaya diri terkirim.
  • Benchmark desain Anda terhadap skrip sekali-pakai yang akan ditulis pengguna Anda. Jika skrip mengalirkan dan Anda menyangga, Anda membangun bentuk yang salah.
  • Perlakukan agregat parsial sebagai jawaban salah yang perlu pelabelan, bukan kredit parsial.

Dokumentasi Workbench membahas permukaan fitur sehari-hari, dan SQL untuk DynamoDB memetakan apa yang berfungsi di seluruh ekosistem. Atau cukup unduh DynoTable, buka sebuah tab Workbench dengan ⌘⌥Q, dan jalankan sebuah JOIN terhadap database yang tidak memilikinya.

Bekerja dengan DynamoDB tanpa Console

DynoTable adalah klien desktop yang cepat untuk DynamoDB — jelajahi tabel, jalankan query gaya SQL, dan edit Item secara lokal.