Seperti yang Anda sebutkan dengan benar, ada berbagai pendekatan dengan berbagai kompleksitas yang melekat pada pelaksanaannya. Ini pada dasarnya mencakup bagaimana hal itu dilakukan dan mana yang Anda terapkan sebenarnya bergantung pada data dan kasus penggunaan mana yang paling cocok untuk Anda.
Pencocokan Rentang Saat Ini
MongoDB 3.6 $lookup
Pendekatan paling sederhana dapat digunakan menggunakan sintaks baru $lookup
operator dengan MongoDB 3.6 yang memungkinkan pipa
untuk diberikan sebagai ungkapan untuk "menggabungkan diri" dengan koleksi yang sama. Ini pada dasarnya dapat menanyakan koleksi lagi untuk item apa pun di mana starttime
"atau" waktu akhir
dari dokumen saat ini berada di antara nilai yang sama dari dokumen lain, tidak termasuk yang asli tentu saja:
db.getCollection('collection').aggregate([
{ "$lookup": {
"from": "collection",
"let": {
"_id": "$_id",
"starttime": "$starttime",
"endtime": "$endtime"
},
"pipeline": [
{ "$match": {
"$expr": {
"$and": [
{ "$ne": [ "$$_id", "$_id" },
{ "$or": [
{ "$and": [
{ "$gte": [ "$$starttime", "$starttime" ] },
{ "$lte": [ "$$starttime", "$endtime" ] }
]},
{ "$and": [
{ "$gte": [ "$$endtime", "$starttime" ] },
{ "$lte": [ "$$endtime", "$endtime" ] }
]}
]},
]
},
"as": "overlaps"
}},
{ "$count": "count" },
]
}},
{ "$match": { "overlaps.0": { "$exists": true } } }
])
Single $lookup
melakukan "gabung" pada koleksi yang sama yang memungkinkan Anda menyimpan nilai "dokumen saat ini" untuk "_id"
, "waktu mulai"
dan "waktu akhir"
nilai masing-masing melalui "let"
pilihan tahap pipa. Ini akan tersedia sebagai "variabel lokal" menggunakan $$
awalan di "pipeline"
berikutnya ekspresi.
Dalam "sub-pipeline" ini, Anda menggunakan $match
tahap pipeline dan $expr
operator kueri, yang memungkinkan Anda mengevaluasi ekspresi logika kerangka agregasi sebagai bagian dari kondisi kueri. Ini memungkinkan perbandingan antar nilai saat memilih dokumen baru yang cocok dengan kondisi.
Syaratnya cukup cari "dokumen yang sudah diproses" dimana "_id"
bidang tidak sama dengan "dokumen saat ini", $and
di mana "starttime"
$or
"waktu akhir"
nilai "dokumen saat ini" berada di antara properti yang sama dari "dokumen yang diproses". Perhatikan di sini bahwa ini serta masing-masing $gte
dan $lte
operator adalah "operator perbandingan agregasi"
dan bukan "operator kueri"
formulir, karena hasil yang dikembalikan dievaluasi oleh $expr
harus boolean
dalam konteks. Inilah yang sebenarnya dilakukan oleh operator perbandingan agregasi, dan ini juga satu-satunya cara untuk meneruskan nilai untuk perbandingan.
Karena kami hanya menginginkan "hitungan" kecocokan, $hitung
tahap pipa digunakan untuk melakukan ini. Hasil keseluruhan $lookup
akan menjadi larik "elemen tunggal" di mana ada hitungan, atau "array kosong" di mana tidak ada kecocokan dengan ketentuan.
Kasus alternatifnya adalah "menghilangkan" $count
panggung dan cukup izinkan dokumen yang cocok untuk kembali. Ini memungkinkan identifikasi yang mudah, tetapi sebagai "array yang disematkan di dalam dokumen" Anda perlu memperhatikan jumlah "tumpang tindih" yang akan dikembalikan sebagai keseluruhan dokumen dan ini tidak menyebabkan pelanggaran batas BSON 16MB. Dalam kebanyakan kasus ini seharusnya baik-baik saja, tetapi untuk kasus di mana Anda mengharapkan banyak tumpang tindih untuk dokumen tertentu, ini bisa menjadi kasus nyata. Jadi ini benar-benar sesuatu yang lebih untuk diperhatikan.
$lookup
tahap pipa dalam konteks ini akan "selalu" mengembalikan array dalam hasil, bahkan jika kosong. Nama properti keluaran "penggabungan" ke dalam dokumen yang ada akan menjadi "overlaps"
sebagaimana ditentukan dalam "sebagai"
properti ke $lookup
panggung.
Mengikuti $lookup
, kita kemudian dapat melakukan $match
sederhana
dengan ekspresi kueri reguler menggunakan $exists
tes untuk 0
nilai indeks larik keluaran. Di mana sebenarnya ada beberapa konten dalam larik dan oleh karena itu "tumpang tindih" kondisinya akan benar dan dokumen dikembalikan, menunjukkan jumlah atau dokumen "tumpang tindih" sesuai pilihan Anda.
Versi lain - Kueri untuk "bergabung"
Kasus alternatif di mana MongoDB Anda tidak memiliki dukungan ini adalah untuk "bergabung" secara manual dengan mengeluarkan kondisi kueri yang sama yang diuraikan di atas untuk setiap dokumen yang diperiksa:
db.getCollection('collection').find().map( d => {
var overlaps = db.getCollection('collection').find({
"_id": { "$ne": d._id },
"$or": [
{ "starttime": { "$gte": d.starttime, "$lte": d.endtime } },
{ "endtime": { "$gte": d.starttime, "$lte": d.endtime } }
]
}).toArray();
return ( overlaps.length !== 0 )
? Object.assign(
d,
{
"overlaps": {
"count": overlaps.length,
"documents": overlaps
}
}
)
: null;
}).filter(e => e != null);
Ini pada dasarnya adalah logika yang sama kecuali kita sebenarnya perlu "kembali ke database" untuk mengeluarkan kueri agar sesuai dengan dokumen yang tumpang tindih. Kali ini "operator kueri" yang digunakan untuk menemukan di mana nilai dokumen saat ini berada di antara nilai dokumen yang diproses.
Karena hasilnya sudah dikembalikan dari server, tidak ada batasan batasan BSON untuk menambahkan konten ke output. Anda mungkin memiliki batasan memori, tapi itu masalah lain. Sederhananya kita mengembalikan array daripada kursor melalui .toArray()
jadi kami memiliki dokumen yang cocok dan cukup mengakses panjang array untuk mendapatkan hitungan. Jika Anda tidak benar-benar membutuhkan dokumen, gunakan .count()
bukannya .find()
jauh lebih efisien karena tidak ada dokumen yang mengambil overhead.
Outputnya kemudian digabungkan dengan dokumen yang ada, di mana perbedaan penting lainnya adalah karena tesis adalah "beberapa kueri", tidak ada cara untuk memberikan kondisi bahwa mereka harus "cocok" dengan sesuatu. Jadi ini membuat kita mempertimbangkan akan ada hasil di mana hitungan ( atau panjang array ) adalah 0
dan yang bisa kita lakukan saat ini adalah mengembalikan null
nilai yang nantinya bisa kita .filter()
dari larik hasil. Metode lain untuk mengulangi kursor menggunakan prinsip dasar yang sama yaitu "membuang" hasil di mana kita tidak menginginkannya. Tapi tidak ada yang menghentikan kueri yang dijalankan di server dan pemfilteran ini adalah "pemrosesan pasca" dalam beberapa bentuk atau lainnya.
Mengurangi Kompleksitas
Jadi pendekatan di atas bekerja dengan struktur seperti yang dijelaskan, tetapi tentu saja kompleksitas keseluruhan mengharuskan untuk setiap dokumen Anda pada dasarnya harus memeriksa setiap dokumen lain dalam koleksi untuk mencari tumpang tindih. Oleh karena itu saat menggunakan $lookup
memungkinkan untuk beberapa "efisiensi" dalam pengurangan overhead transportasi dan respons, masih mengalami masalah yang sama bahwa pada dasarnya Anda masih membandingkan setiap dokumen dengan semuanya.
Solusi yang lebih baik "di mana Anda dapat membuatnya sesuai" adalah sebagai gantinya menyimpan perwakilan "nilai keras"* dari interval pada setiap dokumen. Misalnya kita dapat "menganggap" bahwa ada periode "pemesanan" yang solid selama satu jam dalam sehari dengan total 24 periode pemesanan. Ini "bisa" direpresentasikan seperti:
{ "_id": "A", "booking": [ 10, 11, 12 ] }
{ "_id": "B", "booking": [ 12, 13, 14 ] }
{ "_id": "C", "booking": [ 7, 8 ] }
{ "_id": "D", "booking": [ 9, 10, 11 ] }
Dengan data yang diatur seperti itu di mana ada indikator yang ditetapkan untuk interval, kompleksitasnya sangat berkurang karena itu benar-benar hanya masalah "pengelompokan" pada nilai interval dari array dalam "booking"
properti:
db.booking.aggregate([
{ "$unwind": "$booking" },
{ "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
{ "$match": { "docs.1": { "$exists": true } } }
])
Dan hasilnya:
{ "_id" : 10, "docs" : [ "A", "D" ] }
{ "_id" : 11, "docs" : [ "A", "D" ] }
{ "_id" : 12, "docs" : [ "A", "B" ] }
Itu dengan benar mengidentifikasi itu untuk 10
dan 11
interval keduanya "A"
dan "D"
mengandung tumpang tindih, sementara "B"
dan "A"
tumpang tindih pada 12
. Interval dan pencocokan dokumen lainnya dikecualikan melalui $exists
tes kecuali kali ini pada 1
index ( atau elemen array kedua yang ada ) untuk melihat bahwa ada "lebih dari satu" dokumen dalam pengelompokan, sehingga menunjukkan tumpang tindih.
Ini hanya menggunakan $unwind
tahap pipa agregasi untuk "mendekonstruksi/mendenormalisasi" konten array sehingga kami dapat mengakses nilai dalam untuk pengelompokan. Inilah yang terjadi di $group
tahap di mana "kunci" yang diberikan adalah id interval pemesanan dan $push
operator digunakan untuk "mengumpulkan" data tentang dokumen saat ini yang ditemukan di grup itu. $match
adalah seperti yang dijelaskan sebelumnya.
Ini bahkan dapat diperluas untuk presentasi alternatif:
db.booking.aggregate([
{ "$unwind": "$booking" },
{ "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
{ "$match": { "docs.1": { "$exists": true } } },
{ "$unwind": "$docs" },
{ "$group": {
"_id": "$docs",
"intervals": { "$push": "$_id" }
}}
])
Dengan keluaran:
{ "_id" : "B", "intervals" : [ 12 ] }
{ "_id" : "D", "intervals" : [ 10, 11 ] }
{ "_id" : "A", "intervals" : [ 10, 11, 12 ] }
Ini adalah demonstrasi yang disederhanakan, tetapi jika data yang Anda miliki memungkinkannya untuk jenis analisis yang diperlukan, maka ini adalah pendekatan yang jauh lebih efisien. Jadi, jika Anda dapat mempertahankan "perincian" untuk diperbaiki ke interval "setel" yang dapat direkam secara umum pada setiap dokumen, maka analisis dan pelaporan dapat menggunakan pendekatan terakhir untuk mengidentifikasi tumpang tindih tersebut dengan cepat dan efisien.
Pada dasarnya, ini adalah bagaimana Anda akan mengimplementasikan apa yang pada dasarnya Anda sebutkan sebagai pendekatan "lebih baik", dan yang pertama adalah peningkatan "sedikit" dari apa yang Anda teorikan semula. Lihat mana yang benar-benar sesuai dengan situasi Anda, tetapi ini harus menjelaskan implementasi dan perbedaannya.