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:
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.

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 DISTINCTdiam-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
NULLSQL literal.CASE,CAST, subquery skalar — fallback emitter untuk node sintaks yang tak dikenal adalahreturn 'NULL'. Query berjalan, mengembalikan jawaban salah yang penuh percaya diri, dan tidak memunculkan error. - Kapitalisasi identifier tidak berperilaku seperti SQL.
SELECT PLATFORM FROM tmengembalikan null ketika atributnya adalahplatform— 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, sebuahORDER 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: precisionalih-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
namedari"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.
NULLdiam-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.


