DynamoDB için nasıl yerel bir SQL motoru kurduk (JOIN'i olmayan bir veritabanı)
DynamoDB, 100 GB'lık bir tabloyu dışa aktarabilir ama onu toplayamaz.
JOIN yok, GROUP BY yok, COUNT(*) yok — bunların yavaş sürümleri
değil, hiçbiri — ve AWS'nin kendi SQL benzeri yüzeyi olan
bile bunları eklemez.
Dolayısıyla her ekip aynı tek kullanımlık betiği yazmaya varır: bir
'i sayfalandır, satırları bir map'e katla, cevabı yazdır,
betiği sil.
DynoTable'ın Workbench'i, o betiğe verdiğimiz yanıttır:
hiçbirini desteklemeyen tablolara karşı tek bir gerçek SELECT — çok
tablolu JOIN … ON, WHERE, GROUP BY, HAVING, DISTINCT, CASE,
toplamalar — yazdığınız bir sekme. Bu yazı, kaputun altında nasıl çalıştığı,
bize sessizce yalan söyleyen ayrıştırıcı ve daha sonra motorun bellek
modelini neden söküp attığımızdır.
Bir sorgu motoru kurmayın
Temel karar, bir ilişkisel yürütücü yazmayı reddetmekti. DynamoDB sayfaları gömülü bir SQLite veritabanına akar ve dünyanın en çok test edilmiş sorgu motorlarından biri olan SQLite, ilişkisel işi yapar:
Her DynamoDB öğesi, SQLite'a tek bir item sütununda ham türlü zarfı
({platform: {S: "ios"}}) olarak iner. Küçük bir kullanıcı tanımlı
fonksiyon, attribute'ları sorgu zamanında açar, böylece SELECT platform'unuz
bir attr(item, 'platform', …) çağrısına derlenir. O fonksiyondaki bir
ayrıntı yük taşır: JSON ayrıştırmasını bir catch içine sarar, çünkü SQLite
bağlaması, bozuk bir satırın ayrıştırma hatasını tüm prepared statement'ın
iptaline dönüştürür. Catch olmadan, tek bir bozuk satır sorguyu öldürürdü.
Koruduğumuz dürüst kısıt: DynamoDB'nin erişim deseni fiziği hâlâ
geçerlidir. Bir JOIN'in hedef tarafı bir primary key veya bir
partition anahtarı olmalıdır — motor, birleştirmeleri akışa
karşı anahtar araması ile çözer, asla çapraz çarpım taraması ile değil.
Anahtar olmayan attribute'lar üzerinde keyfi birleştirmelere ihtiyacınız
varsa, bu bir sorgu motoru özelliği değil, bir
modelleme sohbetidir.

Bize yalan söyleyen ayrıştırıcı
İlk sürüm, popüler bir hazır SQL ayrıştırıcısı kullandı. Birkaç hafta içinde, dört bağımsız doğruluk hatası ona kadar izlendi:
SELECT DISTINCTsessizce düşürüldü. Ayrıştırıcı onu kabul etti ve yayıcı onu görmezden geldi — her yinelenen satır geri geldi. Bir kullanıcının hata raporu şöyleydi: "sayaçları değil, tüm değerlerin bir tablosunu gösteriyor."- Ele alınmayan her şey, düz SQL
NULL'ı oldu.CASE,CAST, skaler alt sorgular — yayıcının bilinmeyen bir sözdizim düğümü için geri dönüşüreturn 'NULL''dı. Sorgular çalıştı, kendinden emin yanlış cevaplar döndürdü ve hiçbir hata vermedi. - Tanımlayıcı büyük/küçük harfi SQL gibi davranmadı.
SELECT PLATFORM FROM t, attributeplatformolduğunda null döndürdü — her SQL veritabanının beklemeniz için sizi eğittiği şeyin tam tersi. - Otomatik tamamlama çözümleyiciyle anlaşamadı, derleyicinin ardından çözmeyi başaramayacağı adları önerdi.
Yerine geçen sql-parser-cst, incelemenin başka hiçbir yerde bulamadığı
tek bir ilkelde kazandı: ağacı, her tanımlayıcının hem ham metnini hem
de normalleştirilmiş adını korur — böylece derleyici platform'u
"PLATFORM"'dan ayırt edebilir. O tek ayrım, tek bir dilbilgisinin düzgün
ANSI semantiği taşımasına olanak tanır: tırnaksız tanımlayıcılar
büyük/küçük harf duyarsız çözülür, tırnaklı olanlar tam olarak Postgres
veya SQLite'taki gibi büyük/küçük harf duyarlı kaçış kapısıdır. Ayrıca her
düğümde bir kaynak aralığı taşır, böylece bir tanı, ifadenin tamamı yerine
sorunlu yapının altını çizer.
Ve return 'NULL' geri dönüşünü, artık politika olarak ele aldığımız bir
kuralla değiştirdik: asla sessiz NULL yok. Yayıcının varsayılan kolu,
hassas aralıklı bir hatadır. Pencere fonksiyonları, yalnızca hayır değil,
nedenini söyleyen bir mesaj alır:
Window functions are not supported in Workbench.
They have undefined semantics on the joined-row materialization.Kabul ettiğimiz takas: 1.0 öncesi bir bağımlılık, tam sürüme sabitlenmiş, yalnızca test paketi kapı olacak şekilde elle yükseltilir. Eski ayrıştırıcıyı yamalamayı (paralel bir ön-tokenleştiriciye ihtiyaç duyuyordu — her girdiyi iki kez ayrıştırma), editörün sözdizim ağacını kullanmayı (token düzeyinde, cümle yapısı yok) ve elle yazmayı (bir SQLite ayrıştırıcısının çoğu, sınırsız bakım) değerlendirdik. Dört yönlü bir doğruluk başarısızlığı, bu endişelerin hepsini yener.
Neyi engellememek gerektiğini bilmek
Bir tanı, sizi kasıtlı olarak durdurmaz. Bir filtre, şemanın büyük/küçük harf durumunu teyit edemediği bir attribute'a başvurduğunda, kanonik durumu bilemeyiz — DynamoDB'nin sunucu tarafı filtreleri büyük/küçük harf duyarlıdır ve şeması yalnızca anahtar attribute'ları bildirir. Bu bir uyarı verir ve uyarılar Run'ı asla devre dışı bırakmaz. Bu tür dersi zor yoldan doğrulayıcı çalışmamızda öğrendik: en yaygın meşru sorgu biçimini reddeden bir kapı, kapı olmamasından daha kötüdür.
Üst sınır yanlış biçimdeydi
İlk toplama motoru, taranan satırları bir boyut üst sınırı olan bellek içi bir SQLite'a tamponladı — burada 64 MB, orada 250 MB ve büyük bir tabloda uygulamayı OOM edebilecek üst sınırsız bir yol. Üst sınıra ulaşırsanız, bir taşma bayrağıyla kısmi bir cevap alırdınız.
Yeniden tasarım, bir yeniden çerçevelemeyle başladı: rakip, bir geliştiricinin bunun yerine yazacağı tek kullanımlık betiktir. Ve yetkin bir betik tamponlayıp-sonra-sınırlamaz — akıtır, her sayfayı sınırlı bellekle çalışan bir toplama katlar. Bellek içi üst sınır çok küçük değildi; yanlış biçimdeydi. Daha kötüsü, kısmi bir toplama "eksik" değildir — yanlıştır. Bunu somutlaştıran an: kendi AI demomuz, kısmi bir "en çok harcayanlar" sıralamasını kendinden emin bir şekilde gerçek olarak anlattı.
Yeniden kurulan Compute motoru, betiğin çalıştığı şekilde çalışır, artı betiğin asla uğraşmadığı her şey:
- Bir planlayıcı her sorguyu üç şeritten birine sınıflandırır:
stream-fold (düz cebirsel toplamalar —
COUNT/SUM/AVG/MIN/MAX— ana süreç dışında sayfa sayfa katlanır; boyut, katlanabilir bir sorguyu asla kapılamaz, yalnızca daha uzun sürer), materialize (sıralamalar, anahtar sınırlı birleştirmeler — bellek üst sınırı olmadan diske dayalı SQLite'a taşınır) veya sunamadığımız kuyruk için doğru ağır aracı (DynamoDB'nin S3 dışa aktarması artı Athena) öneren dürüst bir reddetme. - Fold sınıflandırması kasıtlı olarak dardır. Bir
HAVING, birORDER BY, toplamanın yanında çıplak bir sütun — bunlardan herhangi biri materialize şeridini zorlar. Bir cümleyi görmezden gelip yine de "kesin" bildiren bir fold, tam da bu planlayıcının önlemek için var olduğu yanlış-ama-kendinden-emin hata olurdu. - Kesinlik, sonucun bir parçasıdır. Toplama sütunları, sayfalar hâlâ
akarken bir partial rozeti gösterir ve yalnızca tarama boşaldığında onu
düşürür. Aritmetik dürüstlük bile izlenir: tam sayı toplamları 2⁵³'ün
ötesinde kesin kalır (64 bit tam sayı olarak saklanır), oysa float
mantisini aşan kesirli bir toplam, sessizce yuvarlamak yerine kendini
partial: precisionolarak işaretler. - Sigortalar, makineler için açılır pencerelerin yerini alır. Etkileşimli uygulama, beş milyon öğelik bir taramadan önce sorar; AI asistanı ve MCP çağıranları bunun yerine tarama bütçeleri ve sayfa sınırı iptalleri alır. Ve modelin gördüğü sonuç zarfı kasıtlı olarak kırpılmıştır — maliyet sınıfı ve boyut tahminleri saklanır, çünkü eski bir tahmini yüzeye çıkarmak, modeli onu gerçek olarak anlatmaya davet eder.
İçeride insanları şaşırtan bir maliyet notu: tek seferlik bir toplama için, canlı bir tarama bir DynamoDB→S3 dışa aktarması oluşturmaktan kabaca 6× daha ucuzdur — bu yüzden betik biçimli şerit varsayılandır ve veri ambarı, refleks değil, tekrarlanan ağır analitikler için öneridir. (Fiyatlandırma hesaplayıcısı, kendi tablonuzun tam bir geçişini fiyatlandıracaktır.)
SQL'i production'a yöneltmek güvenli mi?
Workbench yapısal olarak salt okunurdur. Yerel motora ulaşan nihai SQL, tek
bir SELECT olmayan her şeyi tokenleştiren ve reddeden derinlemesine
savunma kapısından geçer — bir kullanıcı hatası değil, bir sözleşme
ihlali olarak, çünkü derleyici onu asla üretmemeliydi. SQLite alt katmanı,
tehlikeli yerleşikler devre dışı bırakılmış olarak çalışır ve DynamoDB'ye
yazmaların tüm uygulamada tam olarak bir yolu vardır: SQL'in ulaşamadığı
incelenebilir hazırlama alanı.
Bir tane kuruyorsanız ne aktarılır
- Bir sorgu motoru yazmayın; zaten var olan birine sadık bir derleyici yazın. Hatalarınız, birleştirme algoritmasında değil, dikişlerde (tanımlayıcı büyük/küçük harfi, tür zarfları, bozuk satırlar) yaşayacak.
- Ayrıştırıcınızdan tırnak-kökeni talep edin.
name'i"NAME"'den ayırt edemiyorsa, SQL'in büyük/küçük harf semantiğini uygulayamazsınız ve kullanıcılarınız bunu sizden önce öğrenir. - "Ele alınmayan sözdizimini" asla bir varsayılan değer değil, gürültülü
bir hata yapın. Sessiz
NULL, kendinden emin yanlış cevapların gönderilme şeklidir. - Tasarımınızı, kullanıcınızın yazacağı tek kullanımlık betiğe karşı ölçün. Betik akıtıyorsa ve siz tamponluyorsanız, yanlış biçimi kurmuşsunuzdur.
- Kısmi toplamaları, kısmi puan değil, etiketleme gerektiren yanlış cevaplar olarak ele alın.
Workbench dokümanları günlük özellik yüzeyini kapsar ve
DynamoDB için SQL, tüm ekosistemde neyin işe
yaradığını haritalar. Ya da sadece DynoTable'ı indirin,
⌘⌥Q ile bir Workbench sekmesi açın ve bir JOIN'i, ona sahip
olmayan veritabanına karşı çalıştırın.


