Urutan tampilan menentukan kualitas hampir semua laporan: lima penjualan teratas tidak berarti tanpa urutan, dan daftar buku yang melompat-lompat terasa acak. Tahap $sort mengurutkan document di titik mana pun dalam pipeline — sebelum $group untuk menentukan siapa yang “pertama”, atau sesudahnya untuk meranking hasil perhitungan. Perilakunya identik dengan method sort() pada find(), dengan satu hal tambahan yang perlu kita pahami: batas memori per tahap.

Urutan yang Sama dengan sort() pada find() #

$sort menerima document berisi field kunci dan arah: 1 menaik, -1 menurun. Urutan pembandingnya mengikuti urutan BSON yang sudah kita kenal dari Section Query Lanjutan — MinKey < null < angka < string < object < array < BinData < ObjectId < boolean < date < timestamp < MaxKey. Kita lihat langsung pada data toko buku:

js
use latihan_agregasi
db.dropDatabase()

Output yang Dihasilkan:

text
switched to db latihan_agregasi
{ ok: 1, dropped: 'latihan_agregasi' }
js
db.buku.insertMany([
  { _id: 1, judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "Fiksi", tag: ["sastra", "novel"], stok: 12, tahun_terbit: 2017, terjual: [ { bulan: "Januari", jumlah: 14 }, { bulan: "Februari", jumlah: 9 }, { bulan: "Maret", jumlah: 11 } ] },
  { _id: 2, judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "Fiksi", tag: ["novel", "fantasi"], stok: 20, tahun_terbit: 2014, terjual: [ { bulan: "Januari", jumlah: 21 }, { bulan: "Februari", jumlah: 17 }, { bulan: "Maret", jumlah: 25 } ] },
  { _id: 3, judul: "Supernova: Ksatria, Puteri, dan Bintang Jatuh", penulis: "Dee Lestari", harga: 95000, kategori: "Fiksi", tag: ["novel", "sains"], stok: 6, tahun_terbit: 2001, terjual: [ { bulan: "Januari", jumlah: 7 }, { bulan: "Februari", jumlah: 5 } ] },
  { _id: 4, judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "Nonfiksi", tag: ["filsafat", "pengembangan diri"], stok: 8, tahun_terbit: 2018, terjual: [ { bulan: "Januari", jumlah: 12 }, { bulan: "Februari", jumlah: 15 }, { bulan: "Maret", jumlah: 10 } ] },
  { _id: 5, judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "Nonfiksi", tag: ["pengembangan diri", "kebiasaan"], stok: 15, tahun_terbit: 2018, terjual: [ { bulan: "Januari", jumlah: 18 }, { bulan: "Februari", jumlah: 22 }, { bulan: "Maret", jumlah: 16 } ] },
  { _id: 6, judul: "Kamus Bahasa Indonesia", penulis: "Badan Bahasa", harga: 55000, kategori: "Referensi", tag: ["referensi", "bahasa"], stok: 30, tahun_terbit: 2016, terjual: [] }
])

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedIds: { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6 }
}
js
db.buku.aggregate([
  { $sort: { harga: -1 } },
  { $project: { _id: 0, judul: 1, harga: 1, tahun_terbit: 1 } }
])

Output yang Dihasilkan:

text
[
  { judul: 'Atomic Habits', harga: 115000, tahun_terbit: 2018 },
  { judul: 'Laut Bercerita', harga: 98000, tahun_terbit: 2017 },
  {
    judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
    harga: 95000,
    tahun_terbit: 2001
  },
  { judul: 'Filosofi Teras', harga: 89000, tahun_terbit: 2018 },
  { judul: 'Bumi', harga: 89000, tahun_terbit: 2014 },
  { judul: 'Kamus Bahasa Indonesia', harga: 55000, tahun_terbit: 2016 }
]

Buku tersusun dari Atomic Habits (115000) sampai Kamus Bahasa Indonesia (55000). Perhatikan posisi Filosofi Teras dan Bumi — keduanya berharga sama, 89000, dan urutannya satu sama lain tidak ditentukan oleh kunci tunggal harga.

Multi-Key untuk Urutan yang Stabil #

Seri yang sama menghasilkan urutan ambigu, dan ambigu berarti hasil bisa berubah antar eksekusi. Solusinya menambahkan kunci kedua sebagai pemutus seri:

js
db.buku.aggregate([
  { $sort: { harga: 1, tahun_terbit: -1 } },
  { $project: { _id: 0, judul: 1, harga: 1, tahun_terbit: 1 } }
])

Output yang Dihasilkan:

text
[
  { judul: 'Kamus Bahasa Indonesia', harga: 55000, tahun_terbit: 2016 },
  { judul: 'Filosofi Teras', harga: 89000, tahun_terbit: 2018 },
  { judul: 'Bumi', harga: 89000, tahun_terbit: 2014 },
  {
    judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
    harga: 95000,
    tahun_terbit: 2001
  },
  { judul: 'Laut Bercerita', harga: 98000, tahun_terbit: 2017 },
  { judul: 'Atomic Habits', harga: 115000, tahun_terbit: 2018 }
]

Sekarang seri 89000 tuntas: Filosofi Teras (2018) berada di depan Bumi (2014) karena tahun_terbit: -1 meminta tahun terbaru lebih dulu. Aturannya sama seperti compound sort pada find(): kunci pertama dipakai utama, kunci kedua hanya berbicara saat kunci pertama seri, dan seterusnya.

$sort Setelah $group #

Arah paling umum di pipeline laporan adalah mengurutkan hasil perhitungan. Total stok per kategori dari Chapter $group, misalnya, baru bisa diranking setelah $group selesai menghitung:

js
db.buku.aggregate([
  { $group: { _id: "$kategori", totalStok: { $sum: "$stok" } } },
  { $sort: { totalStok: -1 } }
])

Output yang Dihasilkan:

text
[
  { _id: 'Fiksi', totalStok: 38 },
  { _id: 'Referensi', totalStok: 30 },
  { _id: 'Nonfiksi', totalStok: 23 }
]

Kunci totalStok tidak ada di document asli — field itu lahir dari $group, dan $sort di posisi ini bekerja pada field hasil hitungan. Karena $group tidak menjamin urutan keluaran (kita buktikan di Chapter sebelumnya), $sort setelah $group praktis menjadi kewajiban untuk setiap laporan yang dibaca manusia.

Anggaran Memori 100 MB dan allowDiskUse #

Mengurutkan berarti menyimpan seluruh document di memori sampai urutan final terbentuk — inilah sebabnya $sort punya anggaran memori per tahap sebesar 100 MB. Pada pipeline kecil kita anggaran itu berlimpah, tetapi pada collection besar anggaran bisa terlampaui. MongoDB modern menangani keadaan itu dengan menumpahkan kelebihan pekerjaan ke disk: sejak versi 6.0, parameter server allowDiskUseByDefault bernilai true, sehingga tahap yang kehabisan anggaran otomatis memakai berkas sementara. Perilaku ini bisa kita kendalikan lewat opsi kedua aggregate():

js
db.buku.aggregate(
  [ { $sort: { harga: -1 } }, { $project: { _id: 0, judul: 1, harga: 1 } } ],
  { allowDiskUse: true }
)

Output yang Dihasilkan:

text
[
  { judul: 'Atomic Habits', harga: 115000 },
  { judul: 'Laut Bercerita', harga: 98000 },
  {
    judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
    harga: 95000
  },
  { judul: 'Filosofi Teras', harga: 89000 },
  { judul: 'Bumi', harga: 89000 },
  { judul: 'Kamus Bahasa Indonesia', harga: 55000 }
]

Opsi { allowDiskUse: true } pada dasarnya menegaskan perilaku bawaan di 9.0 — berguna saat kita ingin kode eksplisit, atau saat kebijakan default server diubah administrator. Sebaliknya, { allowDiskUse: false } memaksa pipeline gagal dengan error QueryExceededMemoryLimitNoDiskUseAllowed ketika anggaran 100 MB terlampaui — opsi ini kadang dipilih agar pipeline yang boros terdeteksi dini, bukan diam-diam melambat karena aktivitas disk.

Rangkuman #

  • $sort: { field: 1|-1, ... } mengurutkan document dengan urutan BSON yang sama seperti sort() pada find().
  • Kunci tambahan menjadi pemutus seri; kunci tunggal menghasilkan urutan ambigu untuk nilai yang sama.
  • $sort setelah $group mengurutkan field hasil perhitungan — dan praktis wajib karena $group tidak menjamin urutan.
  • Setiap tahap pengurutan beranggaran 100 MB; sejak 6.0 kelebihannya otomatis tumpah ke disk (allowDiskUseByDefault: true), dan allowDiskUse pada opsi aggregate() mengendalikannya secara eksplisit.