Aplikasi tidak perlu tahu datanya tersebar — mongos merangkum semuanya sebagai satu collection biasa. Tetapi di balik kemudahan itu, kualitas sebuah query di sharded cluster ditentukan satu hal: apakah filternya menyertakan shard key. Query yang menyertakannya meluncur ke shard yang tepat; query yang tidak, disiarkan ke semua shard dan menunggu yang terlama. Chapter ini membedah kedua jalur itu, perilaku sort/limit/skip, dan cara membacanya lewat explain.

Targeted vs Scatter-Gather #

Targeted query menyertakan shard key (atau prefiksnya bila compound) di filter. mongos menghitung chunk mana yang memuat rentang nilai tersebut — lewat cache metadata-nya — lalu mengirim query hanya ke shard pemilik chunk. Scatter-gather query tidak menyertakan shard key, sehingga mongos terpaksa menyiarkannya ke semua shard, menunggu seluruh respons, lalu menggabungkan hasilnya. Biaya scatter-gather tumbuh seiring jumlah shard: cluster sepuluh shard membayar sepuluh kali kerja untuk satu query.

Tabel ringkas perilakunya:

Pola query (shard key userId)Perilaku
find({ userId: 42 })Targeted ke satu shard
find({ userId: { $in: [1, 5, 9] } })Targeted ke shard pemilik tiap nilai
find({ userId: { $gte: 10, $lt: 20 } }) (ranged)Targeted ke shard pemilik rentang
find({ userId: { $gte: 10 } }) (hashed)Scatter-gather — rentang tidak berpeta ke hash
find({ email: "[email protected]" })Scatter-gather ke semua shard
find({ _id: ObjectId(...) }) saat _id bukan shard keyScatter-gather
insertOne(...)Selalu targeted ke satu shard
updateMany/deleteMany tanpa shard keyBroadcast ke semua shard

Aturan tegasnya menyertainya: updateOne(), replaceOne(), dan deleteOne() wajib menyertakan shard key lengkap atau _id di filternya — tanpa keduanya MongoDB menolak operasi karena tidak bisa menentukan shard mana yang dituju.

Sort, Limit, dan Skip di Tangan mongos #

mongos tidak sekadar meneruskan; ia juga merakit hasil:

  • sort() dieksekusi di tiap shard terlebih dahulu, lalu mongos melakukan merge sort atas hasil yang sudah terurut.
  • limit(n) diteruskan ke setiap shard (tiap shard mengembalikan maksimal n document), lalu mongos menerapkan limit sekali lagi di hasil gabungan.
  • skip(n) tidak bisa diteruskan ke shard — mongos mengambil hasil tanpa lompatan lalu melewati n document saat merakit. Dipadukan dengan limit, mongos meneruskan limit + skip ke shard agar tetap efisien.

Membedah Query dengan explain #

Di server latihan, kita bisa melihat fondasi logikanya: query dengan field ter-index (analogi shard key) memanfaatkan rute langsung, sedangkan query tanpanya memindai semua data (analogi semua shard):

js
db.pesan.insertMany([
  { userId: 1, teks: "halo" },
  { userId: 2, teks: "hai" },
  { userId: 3, teks: "hei" }
])
db.pesan.createIndex({ userId: 1 })
db.pesan.find({ userId: 2 }).explain().queryPlanner.winningPlan

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3d8e0bcf662a3b9ce7172'),
    '1': ObjectId('6ad3d8e0bcf662a3b9ce7173'),
    '2': ObjectId('6ad3d8e0bcf662a3b9ce7174')
  }
}
userId_1
{
  isCached: false,
  stage: 'FETCH',
  nss: 'mdtest_mongodb_sharding_queries_on_sharded_collections.pesan',
  inputStage: {
    stage: 'IXSCAN',
    nss: 'mdtest_mongodb_sharding_queries_on_sharded_collections.pesan',
    keyPattern: { userId: 1 },
    indexName: 'userId_1',
    isMultiKey: false,
    multiKeyPaths: { userId: [] },
    isUnique: false,
    isSparse: false,
    isPartial: false,
    indexVersion: 2,
    direction: 'forward',
    indexBounds: { userId: [ '[2, 2]' ] }
  }
}

Sekarang query pada field di luar index — padanannya query tanpa shard key:

js
db.pesan.find({ teks: "hai" }).explain().queryPlanner.winningPlan

Output yang Dihasilkan:

text
{
  isCached: false,
  stage: 'COLLSCAN',
  filter: { teks: { '$eq': 'hai' } },
  nss: 'mdtest_mongodb_sharding_queries_on_sharded_collections.pesan',
  direction: 'forward'
}

COLLSCAN pada satu shard menjadi scatter-gather pada cluster: semua shard memindai datanya masing-masing. Pada sharded cluster sungguhan, output explain menambahkan lapisan ini — mongos menampilkan tahap SHARD_MERGE serta field shards yang memuat rencana eksekusi tiap shard. Bentuk besarnya seperti ini (ilustratif, bukan hasil eksekusi):

text
winningPlan: {
  stage: 'SHARD_MERGE',
  shards: [
    { shardName: 'shardA', ...-winningPlan lokal IXSCAN... },
    { shardName: 'shardB', ...-winningPlan lokal IXSCAN... }
  ]
}

Untuk aggregation pipeline, explain mengungkap pembagian kerja lebih jauh: field splitPipeline menampilkan stage yang dijalankan di masing-masing shard, dan mergeType menunjukkan lokasi penggabungan — router (mongos), anyShard, atau specificShard.

Rangkuman #

  • Query dengan shard key (atau prefiksnya) menjadi targeted; tanpa shard key menjadi scatter-gather yang mahal seiring bertambahnya shard.
  • updateOne, replaceOne, dan deleteOne wajib menyertakan shard key atau _id; insertOne selalu targeted.
  • sort di-merge oleh mongos, limit diteruskan lalu diterapkan ulang, skip baru diterapkan saat perakitan hasil.
  • explain pada cluster memperlihatkan SHARD_MERGE dan rencana per shard; splitPipeline dan mergeType untuk aggregation.

Perutean berdasar nilai shard key masih bisa dipertajam satu level lagi: mengikat rentang data ke shard tertentu lewat zones.