Orta8 dakikalık okuma

DynamoDB JOIN: Tabloları Nasıl Birleştirirsiniz (ve Neden Genellikle Yapamazsınız)

DynamoDB'de JOIN yoktur. API'nin bir birleştirme operatörü yok, veri modelinin yabancı anahtarları yok ve — çoğu insanı şaşırtan kısım — SQL tadındaki sorgu katmanı olan PartiQL de bir tane eklemez. Bir PartiQL SELECT tam olarak bir tablo okur.

İlişkisel bir veritabanından geldiyseniz, bu çarptığınız ilk duvardır. Bu kılavuz, duvarın neden orada olduğunu, geliştiricilerin bunun yerine yaptığı dört şeyi, gerçekten bir gerçek birleştirmeye ihtiyaç duyduğunuz tek durumu — ve bir tanesini nasıl çalıştıracağınızı kapsar.

DynamoDB JOIN yapabilir mi?

Hayır. DynamoDB tabloları birleştiremez — ne düşük seviyeli API aracılığıyla (GetItem / Query / Scan / BatchGetItem), ne PartiQL aracılığıyla, ne de herhangi bir yerleşik sorgu planlayıcı aracılığıyla, çünkü bir sorgu planlayıcı yoktur. Her okuma tek bir tabloya veya onun indekslerinden birine eşlenir. İki tabloyu eşleşen bir anahtar üzerinde birleştirmek, DynamoDB öğeleri geri verdikten sonra yaptığınız bir şeydir, asla içinde değil.

  • DynamoDB'de JOIN operatörü yoktur. Hiç olmadı.
  • PartiQL'in SELECT'i yalnızca tek tabloludur — gramer kelimenin tam anlamıyla SELECT … FROM {{table}}[.{{index}}] şeklindedir ve onu iki tabloya yöneltmek ValidationException: Only Select from a Single Table or index supported döndürür.
  • AWS'nin önerdiği çözüm, bir birleştirmeye ihtiyaç duymamaktır: normalden çıkarın ya da ilgili öğelerin tek bir istekte getirdiğiniz bir bölümde yaşaması için tek tablo tasarımı kullanın.
  • Gerçek tablolar arası / geçici durum için, birleştirmeyi DynamoDB'nin dışında yaparsınız — uygulamanızda ya da sizin için yapan bir araçla.

DynamoDB birleştirme yapabilir mi?

Hayır. DynamoDB tabloları birleştiremez — ne düşük seviyeli API aracılığıyla (GetItem / Query / Scan / BatchGetItem), ne PartiQL aracılığıyla, ne de herhangi bir yerleşik sorgu planlayıcı aracılığıyla, çünkü bir sorgu planlayıcı yoktur. Her okuma tek bir tabloya veya onun indekslerinden birine eşlenir. İki tabloyu eşleşen bir anahtar üzerinde birleştirmek, DynamoDB öğeleri geri verdikten sonra yaptığınız bir şeydir, asla içinde değil.

Bu, AWS'nin doldurmayı unuttuğu bir boşluk değildir. Bilinçli bir tasarım kararıdır ve bir geçici çözüme uzanmadan önce gerekçeyi anlamak değerlidir.

DynamoDB'nin neden birleştirmesi yoktur

Bir SQL JOIN, veritabanından birden çok tabloyu okumasını ve sorgu anında bir araya getirmesini ister. AWS'nin kendi ilişkisel verileri modelleme kılavuzu maliyeti açıkça belirtir: şöyle bir sorgu

SELECT * FROM Orders
  INNER JOIN Order_Items ON Orders.Order_ID = Order_Items.Order_ID
  INNER JOIN Products    ON Products.Product_ID = Order_Items.Product_ID
  ORDER BY Quantity_on_Hand DESC

esnektir ama "sorgudaki her birleştirme sorgunun çalışma zamanı karmaşıklığını artırır çünkü her tablonun verisinin hazırlanması ve ardından bir araya getirilmesi gerekir." Bu iş sınırsızdır — maliyeti sorguya değil veriye bağlıdır — ki bu tam olarak DynamoDB'nin sahip olmayı reddettiği özelliktir.

Bu yüzden AWS kısıtlamayı tasarıma dahil etti. DynamoDB, onların deyimiyle, "her ikisi de [CPU ve ağ] kısıtlamalarını JOIN'leri ortadan kaldırarak (ve verinin normalden çıkarılmasını teşvik ederek) en aza indirmek ve veritabanı mimarisini bir uygulama sorgusunu bir öğeye tek bir istekle tamamen yanıtlayacak şekilde optimize etmek üzere kurulmuştur." Bunlar, herhangi bir ölçekte tek haneli milisaniyelik gecikme satın alan niteliklerdir: bir DynamoDB okumasının çalışma zamanı maliyeti, tablo boyutundan bağımsız olarak sabittir. Tasarım gereği, plan yapacak bir birleştirme motoru ve yabancı anahtar kavramı yoktur.

"Ama PartiQL SQL'dir, kesin birleştirir?"

Hayır. PartiQL size DynamoDB üzerinde SELECT / INSERT / UPDATE / DELETE söz dizimi verir, ancak SQL ile uyumludur, SQL değildir. Resmi SELECT grameri şöyledir:

SELECT  {{expression}}  [, ...]
FROM    {{table}}[.{{index}}]
[ WHERE {{condition}} ]
[ ORDER BY {{key}} [DESC|ASC], ... ]

FROM bir tablo alır (isteğe bağlı olarak onun indekslerinden biri). İkinci bir FROM tablosu, JOIN, alt sorgu veya CTE yoktur. PartiQL'i iki tabloya yöneltin ve DynamoDB onu reddeder (AWS re:Post'ta bildirildi):

ValidationException: Only Select from a Single Table or index supported

PartiQL'in neden SQL'e benzediği ama onun gibi davranamadığı hakkındaki tam gerekçeyi istiyorsanız, bkz. PartiQL vs SQL.

Geliştiricilerin gerçekten kullandığı 4 geçici çözüm

1. Normalden çıkarın (veriyi içeri kopyalayın)

Aksi takdirde birleştireceğiniz alanları doğrudan öğeye saklayın. Bir Order, sonradan çözeceğiniz bir customerId yerine customerName ve shippingAddress anlık görüntüsünü taşır. Tek okuma, birleştirme yok.

Bedeli yazma anındaki yayılmadır: kaynak değiştiğinde her kopyayı güncellersiniz (genellikle bir DynamoDB Streams işleyicisi aracılığıyla). Okuma karmaşıklığını yazma karmaşıklığıyla takas ediyorsunuz — okuma ağırlıklı bir uygulama için genellikle iyi bir takas.

2. Tek tablo tasarımı (bölümde önceden birleştirin)

İlgili varlıkları tek bir tabloda, paylaşılan bir bölüm anahtarı altına koyun, böylece bir öğe koleksiyonu zaten birleştirilmiş sonuçtur. Bir müşteri ve tüm siparişleri PK = "CUSTOMER#42"'yi paylaşır; tek bir Query müşteri öğesini artı her sipariş öğesini döndürür — "birleştirme" yazma anında zaten gerçekleşti.

Query  PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE      (müşteri)
→ CUSTOMER#42 / ORDER#1001   (bir sipariş)
→ CUSTOMER#42 / ORDER#1002   (bir sipariş)

Bu, bire-çok ilişkilere DynamoDB'nin kanonik yanıtıdır. Tam yol gösterimi tek tablo tasarımı içinde.

3. Uygulama tarafında birleştirme (iki okuma, kodda dikme)

A tablosundan okuyun, geri aldığınız anahtarları alın, B tablosundan okuyun ve iki sonuç kümesini uygulamanızda birleştirin. Bu ilişkisel birleştirme mantığıdır — sadece veritabanı yerine kodunuzda çalışır:

// "Her siparişi müşteri adıyla al" — manuel birleştirme.
const {Items: orders} = await ddb.query({TableName: 'Orders' /* … */});

const customers = await Promise.all(
  orders.map((o) => ddb.getItem({TableName: 'Customers', Key: {id: o.customerId}}))
);

const joined = orders.map((o, i) => ({
  ...o,
  customerName: customers[i].Item?.name
}));

Küçük yayılma için uygundur. Çok sayıda siparişle bir N+1 sorunu olur — siparişleri listelemek için bir okuma, sonra sipariş başına bir okuma — ki bu yavaştır ve okuma kapasitesini yakar. BatchGetItem (sonraki) o ikinci dalgayı tek bir gidiş-dönüşte toplar.

4. BatchGetItem (tek gidiş-dönüş, birden çok tablo)

BatchGetItem API'nin "iki tabloya aynı anda dokunma"ya en yakın geldiği yerdir: tek bir istek "bir veya daha fazla öğenin özniteliklerini bir veya daha fazla tablodan" döndürür, çağrı başına 100 öğe veya 16 MB'a kadar, hangisine önce ulaşırsa. Bir uygulama tarafı birleştirmenin gidiş-dönüşlerini keser — ama bir birleştirme değildir. "İstenen öğeleri birincil anahtarla tanımlarsınız"; bir ON koşulu ve ilişkisel eşleştirme yoktur. Yine de anahtarları önceden bilmeniz ve yanıtları kendiniz dikmeniz gerekir.

Gerçek bir JOIN'in kaçınılmaz olduğu durum

Dört geçici çözüm, üretim okuma yollarını iyi kapsar. Yetersiz kaldıkları yer, geçici, keşif amaçlı, analitik sorgudur — modellemediğiniz olan:

  • Bir Orders tablosu ve bir Customers tablosu genelinde "AB'deki hangi müşteriler geçen ay 500 doların üzerinde bir sipariş verdi?"
  • İki varlık türünü birleştiren tek seferlik bir veri kalitesi kontrolü.
  • Raporlama ve toplamalar (GROUP BY, SUM, COUNT) — ki DynamoDB'nin hiç operatörü yoktur.

Bunlar tam olarak bir bölüme önceden pişiremeyeceğiniz sorgulardır, çünkü tanımı gereği onları soracağınızı bilmiyordunuz. İlişkisel içgüdü — bir JOIN yazmak — burada doğru olandır. DynamoDB onu yerel olarak sunamaz ve PartiQL de sunamaz.

Olağan ağır çözüm, S3'e dışa aktarmak ve Athena ile sorgulamak ya da bir veri ambarına aktarmaktır. Bu, ölçekte gerçek analitik için doğrudur, ancak canlı tablonuza karşı şimdi yanıtlamak istediğiniz bir soru için çok fazla tesisat işidir.

DynoTable'ın SQL Workbench'i ile gerçek bir JOIN çalıştırma

DynoTable, SQL Workbench'i DynamoDB tablolarınız üzerinde gerçek SQL — JOIN, GROUP BY ve toplama işlevleri dahil — çalıştıran bir masaüstü DynamoDB istemcisidir. Öğeleri normal DynamoDB API'si aracılığıyla okur, sonra sorgunun ilişkisel kısımlarını istemcide yürütür. Böylece şunu yazabilirsiniz:

SELECT  c.name, SUM(o.total) AS spend
FROM    Customers c
JOIN    Orders o ON o.customerId = c.id
WHERE   c.region = 'EU'
GROUP BY c.name
HAVING  SUM(o.total) > 500

— ve tanımlı bir ilişkisi olmayan tablolara ve JOIN anahtar sözcüğü olmayan bir sorgu motoruna karşı bir sonuç kümesi alırsınız.

Dürüst uyarı — "DynamoDB'nin erişim deseni kuralları içinde": Workbench yine de DynamoDB aracılığıyla okur, bu yüzden sınırsız bir birleştirme sınırsız bir okumadır. En hızlı sorgular, WHERE yan tümcesinin (ya da birleştirmenin ON özniteliğinin) en az bir tarafta bir bölüm anahtarına veya bir GSI'ya isabet ettiği sorgulardır, böylece DynamoDB birleştirme yürütülmeden önce tam bir tablo taraması yerine bir Query çalıştırır. Workbench bu kılavuzdaki kısıtlamaları kaldırmaz — sadece dikmeyi elle yazmak yerine SQL sorusunu sormanıza izin verir ve altta ne yaptığını size söyler.

"Evet, birleştirebilirsiniz" diyen, aslında doğru olan tek şey budur: PartiQL ve AWS'nin kendi NoSQL Workbench'i — işlem oluşturucusu tek tablolu veri düzlemi işlemleriyle sınırlıdır (Query / Scan / GetItem) — her ikisi de tek tablo duvarında durur, diğer çoğu GUI istemcisi gibi. DynoTable'ın bir DynamoDB GUI olarak nasıl karşılaştırıldığına bakın.

SSS

PartiQL JOIN'i destekler mi? Hayır. PartiQL'in SELECT'i tek bir tabloyu (ya da indekslerinden birini) okur. Çok tablolu bir sorgu ValidationException: Only Select from a Single Table or index supported döndürür. API'nin geri kalanıyla aynı duvar.

Tek bir sorguda iki DynamoDB tablosunu birleştirebilir misiniz? Yerel olarak hayır. DynamoDB API'sinin iki tabloyu okuyup bir anahtar üzerinde eşleştiren bir ifadesi yoktur. BatchGetItem tek bir istekte birden çok tablodan öğe okuyabilir, ama bir ON koşulu yoktur — birincil anahtarla adlandırdığınız öğeleri döndürür ve eşleştirmeyi size bırakır. Gerçek bir JOIN … ON … yalnızca DynamoDB'nin dışında gerçekleşir: uygulamanızda ya da DynoTable'ın SQL Workbench'inde.

Bir tabloyu kendi GSI'sine birleştirebilir misiniz? Hayır — bir Global İkincil İndeks birleştirdiğiniz ayrı bir tablo değildir; aynı öğelerin alternatif bir anahtar görünümüdür. Belirli bir SELECT'te ya tabloyu ya da indeksi Query edersiniz, ikisini birlikte birleştirmezsiniz. Bir GSI, öğelere farklı bir anahtarla ulaşmanızı sağlar ki bu çoğu zaman bir birleştirme ihtiyacını baştan ortadan kaldırır.

İki AWS hesabı genelinde (ya da farklı hesaplardaki iki tablo) birleştirebilir misiniz? Yerel olarak hayır ve BatchGetItem ile de hayır — tek bir istek kimlik bilgilerini kapsayamaz ve hesaplar arası birleştirme ilkeli yoktur. Her tabloyu kendi hesabının kimlik bilgileriyle okur ve sonuçları uygulamanızda ya da DynoTable'ın Workbench'i gibi bir araçta birleştirirsiniz.

Normalden çıkarma gerçekten bir birleştirmeden daha mı iyi? DynamoDB'nin hedef iş yükü için — öngörülebilir, yüksek hacimli okumalar — evet. Maliyeti yazma anına taşırsınız (ve bir miktar veri tekrarını kabul edersiniz) ve karşılığında düz ölçeklenen tek istekli okumalar elde edersiniz. Tek tablo tasarımı kılavuzu takasları kapsar.


Bu okumalar için anahtarları ve koşulları elle oluşturmak zahmetlidir — ifade oluşturucu sizin için KeyConditionExpression / FilterExpression söz dizimini üretir ve DynoTable bir geçici çözüm yetmediğinde gerçek SQL'i çalıştırır.

Güncellendi