Menengah8 menit baca

DynamoDB JOIN: Cara Menggabungkan Tabel (dan Mengapa Biasanya Tidak Bisa)

Tidak ada JOIN di DynamoDB. API tidak punya operator join, model data tidak punya foreign key, dan — bagian yang mengejutkan kebanyakan orang — PartiQL, lapisan query berasa-SQL, juga tidak menambahkannya. Sebuah PartiQL SELECT membaca tepat satu tabel.

Jika Anda datang dari database relasional, ini adalah dinding pertama yang Anda hantam. Panduan ini membahas mengapa dinding itu ada, empat hal yang dilakukan developer sebagai gantinya, satu kasus di mana Anda benar-benar membutuhkan join sungguhan — dan cara menjalankannya.

Bisakah DynamoDB melakukan join?

Tidak. DynamoDB tidak dapat menggabungkan tabel — tidak melalui API tingkat-rendah (GetItem / Query / Scan / BatchGetItem), tidak melalui PartiQL, dan tidak melalui query planner bawaan apa pun, karena tidak ada query planner. Setiap baca memetakan ke satu tabel atau salah satu indeksnya; menggabungkan dua tabel pada key yang cocok adalah sesuatu yang Anda lakukan di aplikasi Anda setelah DynamoDB mengembalikan item, tidak pernah di dalamnya.

  • DynamoDB tidak punya operator JOIN. Tidak pernah punya.
  • SELECT PartiQL adalah tabel-tunggal saja — grammar-nya secara harfiah SELECT … FROM {{table}}[.{{index}}], dan mengarahkannya ke dua tabel mengembalikan ValidationException: Only Select from a Single Table or index supported.
  • Perbaikan yang direkomendasikan AWS adalah tidak membutuhkan join: denormalisasi, atau gunakan single-table design agar item terkait hidup di satu partisi yang Anda ambil dalam satu permintaan.
  • Untuk kasus lintas-tabel / ad-hoc yang sejati, Anda join di luar DynamoDB — di aplikasi Anda, atau dengan alat yang melakukannya untuk Anda.

Mengapa DynamoDB tidak punya join

Sebuah SQL JOIN meminta database membaca beberapa tabel dan merakitnya pada waktu query. Panduan AWS sendiri untuk memodelkan data relasional menjabarkan biayanya: sebuah query seperti

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

fleksibel, tetapi "setiap join dalam query meningkatkan kompleksitas runtime query karena data untuk setiap tabel harus disusun lalu dirakit." Pekerjaan itu tak terbatas — biayanya tergantung pada data, bukan pada query — yang justru merupakan properti yang ditolak DynamoDB.

Jadi AWS merancang batasannya. DynamoDB, dalam kata-kata mereka, "dibangun untuk meminimalkan kedua batasan [CPU dan jaringan] dengan menghilangkan JOIN (dan mendorong denormalisasi data) serta mengoptimalkan arsitektur database untuk sepenuhnya menjawab query aplikasi dengan satu permintaan ke sebuah item." Itulah kualitas yang membeli latensi satu-digit-milidetik pada skala apa pun: biaya runtime baca DynamoDB konstan terlepas dari ukuran tabel. Tidak ada engine join dan tidak ada konsep foreign-key untuk direncanakan — secara desain.

"Tapi PartiQL itu SQL, pasti bisa join?"

Tidak. PartiQL memberi Anda sintaksis SELECT / INSERT / UPDATE / DELETE atas DynamoDB, tetapi ia kompatibel-SQL, bukan SQL. Grammar SELECT resmi adalah:

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

FROM mengambil satu tabel (opsional salah satu indeksnya). Tidak ada tabel FROM kedua, tidak ada JOIN, tidak ada subquery, tidak ada CTE. Arahkan PartiQL ke dua tabel dan DynamoDB menolaknya (dilaporkan di AWS re:Post):

ValidationException: Only Select from a Single Table or index supported

Jika Anda ingin alasan lengkap mengapa PartiQL terlihat seperti SQL tetapi tidak dapat berperilaku seperti itu, lihat PartiQL vs SQL.

4 workaround yang sebenarnya dipakai developer

1. Denormalisasi (salin datanya masuk)

Simpan field yang seharusnya Anda join langsung ke item. Sebuah Order membawa snapshot dari customerName dan shippingAddress alih-alih customerId yang akan Anda resolusi nanti. Satu baca, tanpa join.

Biayanya adalah fan-out waktu-tulis: saat sumber berubah Anda memperbarui setiap salinan (biasanya via handler DynamoDB Streams). Anda menukar kompleksitas baca dengan kompleksitas tulis — biasanya pertukaran bagus untuk aplikasi berat-baca.

2. Single-table design (pra-join di partisi)

Letakkan entitas terkait di satu tabel di bawah partition key bersama sehingga sebuah koleksi item adalah hasil tergabung. Seorang pelanggan dan semua order-nya berbagi PK = "CUSTOMER#42"; satu Query mengembalikan item pelanggan plus setiap item order — "join" sudah terjadi pada waktu tulis.

Query  PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE      (pelanggannya)
→ CUSTOMER#42 / ORDER#1001   (sebuah order)
→ CUSTOMER#42 / ORDER#1002   (sebuah order)

Ini jawaban DynamoDB kanonik untuk relasi satu-ke-banyak. Panduan lengkap di single-table design.

3. Join sisi-aplikasi (dua baca, jahit di kode)

Baca dari tabel A, ambil key yang Anda dapat kembali, baca dari tabel B, dan gabungkan kedua set hasil di aplikasi Anda. Ini logika join relasional — hanya berjalan di kode Anda alih-alih database:

// "Ambil setiap order dengan nama pelanggannya" — join manual.
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
}));

Cukup untuk fan-out kecil. Dengan banyak order ia menjadi masalah N+1 — satu baca untuk mendaftar order, lalu satu baca per order — yang lambat dan membakar kapasitas baca. BatchGetItem (berikutnya) menyatukan gelombang kedua itu menjadi satu round-trip.

4. BatchGetItem (satu round-trip, banyak tabel)

BatchGetItem adalah yang terdekat dari API ke "menyentuh dua tabel sekaligus": satu permintaan mengembalikan "atribut dari satu atau lebih item dari satu atau lebih tabel," hingga 100 item atau 16 MB per panggilan, mana pun yang lebih dulu tercapai. Ia memangkas round-trip dari join sisi-aplikasi — tetapi ia bukan join. Anda "mengidentifikasi item yang diminta berdasarkan primary key"; tidak ada kondisi ON dan tidak ada pencocokan relasional. Anda tetap harus tahu key di muka dan menjahit respons sendiri.

Kapan JOIN sungguhan tak terhindarkan

Keempat workaround mencakup jalur baca produksi dengan baik. Di mana mereka gagal adalah query ad-hoc, eksploratif, analitis — yang tidak Anda modelkan:

  • "Pelanggan mana di EU yang menempatkan order di atas $500 bulan lalu?" lintas tabel Orders dan tabel Customers.
  • Pemeriksaan kualitas-data sekali-pakai yang menggabungkan dua tipe entitas.
  • Reporting dan agregat (GROUP BY, SUM, COUNT) — yang DynamoDB sama sekali tidak punya operatornya.

Ini justru query yang tidak dapat Anda pra-panggang ke dalam partisi, karena menurut definisi Anda tidak tahu Anda akan menanyakannya. Insting relasional — menulis sebuah JOIN — adalah yang tepat di sini. DynamoDB hanya tidak dapat melayaninya secara native, dan PartiQL juga tidak.

Jawaban berat yang biasa adalah mengekspor ke S3 dan query dengan Athena, atau menyalurkan ke warehouse. Itu benar untuk analitik sejati pada skala, tetapi itu banyak pemipaan untuk pertanyaan yang ingin Anda jawab sekarang, terhadap tabel live Anda.

Menjalankan JOIN sungguhan dengan SQL Workbench DynoTable

DynoTable adalah klien DynamoDB desktop yang SQL Workbench-nya menjalankan SQL aktual — termasuk JOIN, GROUP BY, dan fungsi agregat — atas tabel DynamoDB Anda. Ia membaca item melalui API DynamoDB normal, lalu mengeksekusi bagian relasional dari query di klien. Jadi Anda dapat menulis:

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

— dan mendapatkan set hasil, terhadap tabel yang tidak punya relasi terdefinisi dan engine query yang tidak punya keyword JOIN.

Caveat jujur — "dalam aturan pola-akses DynamoDB": Workbench tetap membaca melalui DynamoDB, jadi join tak-terbatas adalah baca tak-terbatas. Query tercepat adalah yang di mana klausa WHERE (atau atribut ON dari join) menghantam partition key atau sebuah GSI di setidaknya satu sisi, sehingga DynamoDB menjalankan Query alih-alih scan tabel penuh sebelum join dieksekusi. Workbench tidak mencabut batasan dalam panduan ini — ia hanya membuat Anda dapat menanyakan pertanyaan SQL alih-alih menulis jahitan dengan tangan sendiri, dan memberi tahu Anda apa yang dilakukannya di bawah.

Ini satu-satunya "ya, Anda bisa join" yang benar-benar nyata: PartiQL dan NoSQL Workbench AWS sendiri — yang operation builder-nya terbatas pada operasi data-plane tabel-tunggal (Query / Scan / GetItem) — keduanya berhenti di dinding tabel-tunggal, seperti juga kebanyakan klien GUI lain. Lihat bagaimana DynoTable dibandingkan sebagai GUI DynamoDB.

FAQ

Apakah PartiQL mendukung JOIN? Tidak. SELECT PartiQL membaca satu tabel (atau salah satu indeksnya). Query multi-tabel mengembalikan ValidationException: Only Select from a Single Table or index supported. Dinding yang sama dengan sisa API.

Bisakah Anda menggabungkan dua tabel DynamoDB dalam satu query? Tidak secara native. API DynamoDB tidak punya pernyataan yang membaca dua tabel dan mencocokkannya pada sebuah key. BatchGetItem dapat membaca item dari banyak tabel dalam satu permintaan, tetapi tidak punya kondisi ON — ia mengembalikan item yang Anda sebut berdasarkan primary key dan menyerahkan pencocokan kepada Anda. Sebuah JOIN … ON … sungguhan hanya terjadi di luar DynamoDB: di aplikasi Anda, atau di SQL Workbench DynoTable.

Bisakah Anda menggabungkan tabel ke GSI-nya? Tidak — sebuah Global Secondary Index bukan tabel terpisah yang Anda gabungkan; ia tampilan key alternatif dari item yang sama. Anda Query entah tabelnya atau indeksnya dalam sebuah SELECT, bukan keduanya digabung. Sebuah GSI membuat Anda dapat menjangkau item dengan key berbeda, yang sering menghilangkan kebutuhan join sejak awal.

Bisakah Anda menggabungkan lintas dua akun AWS (atau dua tabel di akun berbeda)? Tidak secara native, dan tidak dengan BatchGetItem juga — satu permintaan tidak dapat menjangkau kredensial, dan tidak ada primitif join lintas-akun. Anda akan membaca setiap tabel dengan kredensial akunnya sendiri dan menggabungkan hasilnya di aplikasi Anda atau di alat seperti Workbench DynoTable.

Apakah denormalisasi benar-benar lebih baik daripada join? Untuk beban kerja target DynamoDB — baca yang dapat diprediksi, bervolume tinggi — ya. Anda memindahkan biaya ke waktu tulis (dan menerima beberapa duplikasi data) sebagai ganti baca satu-permintaan yang berskala datar. Panduan single-table design membahas trade-off-nya.


Membangun key dan kondisi untuk baca ini dengan tangan itu merepotkan — expression builder menghasilkan sintaksis KeyConditionExpression / FilterExpression untuk Anda, dan DynoTable menjalankan SQL sungguhan saat workaround tidak cukup.

Diperbarui