Menengah9 menit baca

JOIN di DynamoDB: cara menggabungkan tabel

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

Kalau Anda datang dari database relasional, ini adalah tembok pertama yang Anda tabrak. Panduan ini membahas mengapa tembok itu ada, empat hal yang dilakukan developer sebagai gantinya, satu-satunya kasus di mana Anda benar-benar butuh join sungguhan — dan cara menjalankannya.

Bisakah DynamoDB melakukan join?

Tidak. DynamoDB tidak bisa menggabungkan tabel — tidak lewat API tingkat rendah (GetItem / Query / Scan / BatchGetItem), tidak lewat , dan tidak lewat query planner bawaan, karena tidak ada. Setiap baca memetakan ke satu tabel atau salah satu indeksnya; menggabungkan dua tabel pada key yang cocok adalah sesuatu yang Anda lakukan di app setelah DynamoDB mengembalikan item, bukan di dalamnya.

  • DynamoDB tidak punya operator JOIN sama sekali. Tidak pernah.
  • SELECT PartiQL hanya satu tabel — grammar-nya secara harfiah SELECT … FROM {{table}}[.{{index}}], dan mengarahkannya ke dua tabel mengembalikan ValidationException: Only select from a single table or index is supported.
  • Perbaikan yang direkomendasikan AWS adalah tidak perlu join: , atau pakai single-table design agar item terkait hidup di satu partisi yang Anda ambil dalam satu request.
  • Untuk kasus cross-table / ad-hoc yang sungguhan, Anda join di luar DynamoDB — di app, atau dengan tool yang melakukannya untuk Anda.

Mengapa DynamoDB tidak punya join

JOIN SQL meminta database membaca banyak tabel dan merakitnya saat query. Panduan AWS tentang modeling data relasional merinci biayanya: 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
  INNER JOIN Inventories ON Products.Product_ID = Inventories.Product_ID
  ORDER BY Quantity_on_Hand DESC

fleksibel, tetapi «setiap join di query meningkatkan kompleksitas runtime karena data dari setiap tabel harus di-stage lalu dirakit». Pekerjaan itu tanpa batas — biayanya bergantung pada data, bukan query — tepat properti yang DynamoDB menolak miliki.

Jadi AWS merancang constraint-nya di muka. DynamoDB, dalam kata mereka sendiri, «dibangun untuk meminimalkan kedua constraint [CPU dan jaringan] itu dengan menghilangkan JOIN (dan mendorong denormalisasi data) serta mengoptimalkan arsitektur agar menjawab query aplikasi sepenuhnya dengan satu request ke sebuah item». Itulah kualitas yang membeli latensi satu digit milidetik pada skala apa pun: biaya runtime sebuah baca DynamoDB konstan berapa pun ukuran tabel. Tidak ada join engine dan tidak ada konsep foreign key yang bisa direncanakan — by design.

«Tapi PartiQL itu SQL, pasti bisa join?»

Tidak. PartiQL memberi Anda sintaks SELECT / INSERT / UPDATE / DELETE di atas DynamoDB, tetapi itu compatible dengan SQL, bukan SQL. Grammar resmi SELECT:

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. Kami menjalankan ketiga formulasi terhadap DynamoDB untuk melihat tepat bagaimana masing-masing gagal.

JOIN eksplisit:

SELECT o.pk FROM "Orders" o JOIN "Customers" c ON o.customerId = c.pk
ValidationException: Only select from a single table or index is supported.

Dua tabel di FROM — penolakan yang sama, jadi engine tidak menolak keyword JOIN, ia menolak tabel kedua:

SELECT * FROM "Orders", "Customers"
ValidationException: Only select from a single table or index is supported.

Subquery gagal dengan cara berbeda, yang berguna saat debugging. PartiQL bahkan tidak sampai ke cek multi-tabel — ia menolak operand IN lebih dulu, jadi Anda mendapat pesan yang tidak pernah menyebut tabel:

SELECT * FROM "Orders" WHERE customerId IN (SELECT pk FROM "Customers")
ValidationException: IN operator must have a left hand argument of type Variable
Reference and right hand argument of type Seq with at least one member

Konsekuensi praktis: tidak ada formulasi yang memberi Anda join. Dua yang pertama mati di tabel kedua, dan yang ketiga mati lebih awal, di operand.

Kalau Anda ingin seluruh alasan mengapa PartiQL terlihat seperti SQL tetapi tidak bisa berperilaku seperti itu, lihat PartiQL vs SQL.

4 workaround yang benar-benar dipakai developer

1. Denormalisasi (salin data)

Simpan field yang biasanya Anda join langsung di item. Sebuah Order membawa snapshot customerName dan shippingAddress alih-alih customerId yang nanti Anda resolve. Satu baca, tanpa join.

Biayanya fan-out saat tulis: ketika sumber berubah, Anda memperbarui setiap salinan (biasanya lewat handler ). Anda menukar kompleksitas baca dengan kompleksitas tulis — biasanya trade yang bagus untuk app yang read-heavy.

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

Letakkan entity terkait di satu tabel di bawah partition key bersama agar sebuah adalah hasil yang sudah di-join. Customer dan semua order-nya berbagi PK = "CUSTOMER#42"; satu Query mengembalikan item customer plus setiap item order — «join»-nya sudah terjadi saat tulis.

Query  PK = "CUSTOMER#42"
→ CUSTOMER#42 / PROFILE      (the customer)
→ CUSTOMER#42 / ORDER#1001   (an order)
→ CUSTOMER#42 / ORDER#1002   (an order)

Ini jawaban kanonik DynamoDB untuk relasi one-to-many. Penjelasan lengkap di single-table design.

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

Baca dari tabel A, ambil key yang Anda bawa, baca dari tabel B, dan gabungkan kedua result set di aplikasi. Itu logika join relasional — hanya saja berjalan di kode Anda, bukan di database:

// "Get each order with its customer name" — the manual join.
const {Items: orders} = await ddb.query({TableName: 'Orders' /* … */});

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

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

Baik untuk fan-out kecil. Dengan banyak order, ini menjadi masalah N+1 — satu baca untuk daftar order, lalu satu baca per order — lambat dan menghabiskan read capacity. BatchGetItem (di bawah) meruntuhkan gelombang kedua itu jadi satu round-trip.

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

BatchGetItem adalah yang paling dekat API dengan «menyentuh dua tabel sekaligus»: satu request mengembalikan «atribut dari satu atau lebih item dari satu atau lebih tabel», hingga 100 item atau 16 MB per panggilan, mana yang lebih dulu. Memotong round-trip join di app — tetapi bukan join. «Anda mengidentifikasi item yang diminta lewat primary key»; tidak ada kondisi ON atau matching relasional. Anda tetap harus tahu key-nya di muka dan menjahit respons sendiri.

Kapan JOIN sungguhan tak terhindarkan

Empat workaround di atas menutup path baca produksi dengan baik. Di mana mereka jatuh adalah query ad-hoc, eksplorasi, analitik — yang tidak Anda modelkan:

  • «Customer EU mana yang membuat order lebih dari $500 bulan lalu?» lintas tabel Orders dan Customers.
  • Cek kualitas data one-off yang menggabungkan dua tipe entity.
  • Reporting dan agregat (GROUP BY, SUM, COUNT) — yang DynamoDB tidak punya operatornya sama sekali.

Itulah tepat query yang tidak bisa Anda bake ke dalam partisi, karena menurut definisi Anda tidak tahu akan membuatnya. Insting relasional — menulis JOIN — benar di sini. DynamoDB sekadar tidak bisa menyajikannya secara native, dan PartiQL juga tidak.

Jawaban berat yang biasa adalah export ke S3 dan query dengan Athena (atau konektor federated query Athena untuk JOIN ke tabel live), atau menyalurkan ke warehouse. Benar untuk analytics skala besar sungguhan, tetapi banyak plumbing untuk pertanyaan yang Anda ingin jawab sekarang, terhadap tabel live Anda.

Menjalankan JOIN sungguhan dengan SQL Workbench DynoTable

DynoTable adalah client DynamoDB desktop yang SQL Workbench-nya menjalankan SQL sungguhan — termasuk JOIN, GROUP BY, dan fungsi agregasi — terhadap tabel DynamoDB Anda. Ia membaca item lewat API DynamoDB biasa, lalu menjalankan bagian relasional query di klien. Jadi Anda bisa 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 mendapat result set, terhadap tabel yang tidak punya relasi terdefinisi dan query engine yang tidak punya keyword JOIN.

Caveat jujur — «di dalam aturan access pattern DynamoDB»: Workbench tetap membaca lewat DynamoDB, jadi join tanpa batas adalah baca tanpa batas. Query tercepat adalah yang klausul WHERE-nya (atau atribut ON join) mengenai partition key atau GSI di setidaknya satu sisi, agar DynamoDB menjalankan Query alih-alih Scan full-table sebelum join berjalan. Workbench tidak menghapus constraint panduan ini — ia hanya membiarkan Anda mengajukan pertanyaan dalam SQL alih-alih menulis jahitan manual, dan memberitahu apa yang dilakukannya di bawahnya.

Di antara client GUI, ini satu-satunya «ya, Anda bisa join» yang benar-benar benar: PartiQL dan NoSQL Workbench AWS sendiri — yang operation builder-nya menjalankan operasi satu-tabel dan statement PartiQL (tanpa JOIN, tanpa SELECT multi-tabel) — keduanya berhenti di tembok satu-tabel, seperti kebanyakan client 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 is supported. Tembok yang sama dengan sisa API.

Bisakah Anda menggabungkan dua tabel DynamoDB dalam satu query? Tidak secara native. API DynamoDB tidak punya statement yang membaca dua tabel dan mencocokkannya pada sebuah key. BatchGetItem bisa membaca item dari beberapa tabel dalam satu request, tetapi tidak punya kondisi ON — ia mengembalikan item yang Anda sebut lewat primary key dan menyerahkan matching kepada Anda. JOIN … ON … sungguhan hanya terjadi di luar DynamoDB: di app, atau di SQL Workbench DynoTable.

Bisakah Anda menggabungkan tabel dengan GSI-nya? Tidak — global secondary index bukan tabel terpisah yang Anda join; itu tampilan by-key alternatif dari item yang sama. Anda Query atau tabel atau indeks dalam satu SELECT tertentu, bukan keduanya yang di-join. GSI membiarkan Anda mencapai item lewat key berbeda, yang sering menghilangkan kebutuhan join sejak awal.

Bisakah Anda join lintas dua akun AWS (atau dua tabel di akun berbeda)? Tidak secara native — tidak ada primitif join lintas-akun. BatchGetItem bisa mencapai tabel akun lain jika resource-based policy tabel itu memberi akses ke caller Anda (sejak Maret 2024), tetapi tetap tanpa kondisi ON, jadi itu baca multi-tabel, bukan join. Anda akan membaca tiap sisi dan menggabungkan hasil di aplikasi atau tool seperti Workbench DynoTable.

Apakah denormalisasi benar-benar lebih baik daripada join? Untuk workload target DynamoDB — baca prediktabel volume tinggi — ya. Anda memindahkan biaya ke saat tulis (dan menerima sedikit duplikasi data) demi baca satu-request yang skalanya datar. Panduan single-table design menutupi trade-off-nya.


Membangun key dan kondisi untuk baca-baca ini secara manual membosankan — Expression Builder menghasilkan sintaks KeyConditionExpression / FilterExpression untuk Anda, dan DynoTable menjalankan SQL sungguhan ketika workaround tidak cukup.

Pesan penolakan PartiQL direproduksi 11 Agustus 2026 terhadap DynamoDB Local (us-east-1, @aws-sdk/client-dynamodb v3.1096.0), dikutip kata demi kata dari ValidationException.message.

Diperbarui