MongoDB'de bir dizi alanını sorgulamanın dört temel yolu vardır: diziyi birebir eşlemek, dizide belirli bir elemanın bulunup bulunmadığını sormak, $all ile birden çok elemanın hepsinin bulunmasını istemek ve $elemMatch ile aynı elemanın birden çok koşulu birlikte sağlamasını şart koşmak. Dizi uzunluğu için $size, belirli bir konum için ise nokta gösterimiyle indeks numarası ("etiketler.0") kullanılır.

Dizi sorgularındaki en yaygın hata, birden çok koşulun aynı elemana mı yoksa dizinin farklı elemanlarına mı uygulandığını karıştırmaktır. Bu yazıda MongoDB 9.0 belgelerine dayanarak skaler dizileri, gömülü belge dizilerini, dizi elemanlarını seçen projection operatörlerini ve null davranışını örneklerle açıklıyoruz. Dizilere eleman ekleme ve çıkarma işlemleri için $push, $pull ve arrayFilters yazısına bakın.

Örnek veri

Örnek veri

Yazı boyunca iki küçük koleksiyon kullanacağız. makaleler koleksiyonunda metin etiketleri ve sayısal puanlar, depo koleksiyonunda ise her ürün için depo bazında stok kayıtları (gömülü belge dizisi) bulunuyor.

javascript
db.makaleler.insertMany( [
   { _id: 1, baslik: "MongoDB giriş",  etiketler: [ "veritabani", "nosql" ],          puanlar: [ 82, 85, 88 ] },
   { _id: 2, baslik: "İndeksler",      etiketler: [ "nosql", "performans", "indeks" ], puanlar: [ 75, 88, 89 ] },
   { _id: 3, baslik: "Yedekleme",      etiketler: [ "veritabani", "yedek" ],           puanlar: [ 95 ] },
   { _id: 4, baslik: "Taslak",         etiketler: [ ] }
] )

db.depo.insertMany( [
   { urun: "defter", stok: [ { depo: "A", adet: 5 },  { depo: "C", adet: 15 } ] },
   { urun: "kalem",  stok: [ { depo: "C", adet: 5 } ] },
   { urun: "kağıt",  stok: [ { depo: "A", adet: 60 }, { depo: "B", adet: 15 } ] },
   { urun: "ajanda", stok: [ { depo: "A", adet: 40 }, { depo: "B", adet: 5 } ] }
] )

Diziyi birebir eşlemek

{ alan: [ ... ] } biçimindeki bir filtre, alanın verilen diziye tam olarak eşit olmasını ister: aynı elemanlar, aynı sayıda ve aynı sırada. Aşağıdaki ilk sorgu yalnızca 1 numaralı makaleyi eşler; etiketleri ters sırayla yazarsanız hiçbir belge eşleşmez. Bu yazım, dizinin bir değer kümesi değil sıralı bir liste olarak önemli olduğu durumlar içindir.

javascript
db.makaleler.find( { etiketler: [ "veritabani", "nosql" ] } )   // yalnızca _id 1
db.makaleler.find( { etiketler: [ "nosql", "veritabani" ] } )   // eşleşme yok: sıra farklı

Sıra ve ek elemanlar umurunuzda değilse, yani "bu iki etiketin ikisi de dizide olsun" demek istiyorsanız $all kullanın.

Dizide bir eleman aramak

Dizi alanına skaler bir değerle eşitlik koşulu yazdığınızda MongoDB, dizinin en az bir elemanının bu değere eşit olup olmadığına bakar. Yani { etiketler: "nosql" } sorgusu için ayrı bir operatör gerekmez; 1 ve 2 numaralı makaleler eşleşir. Aynı mantık karşılaştırma operatörleri için de geçerlidir: { puanlar: { $gt: 90 } } en az bir puanı 90'ı geçen makaleyi, yani 3 numarayı getirir.

javascript
db.makaleler.find( { etiketler: "nosql" } )           // _id 1 ve 2
db.makaleler.find( { puanlar: { $gt: 90 } } )         // _id 3
db.makaleler.find( { etiketler: { $in: [ "yedek", "indeks" ] } } )   // _id 2 ve 3

$in bir dizi alanına uygulandığında, dizinin en az bir elemanı listedeki değerlerden biriyle eşleşen belgeleri seçer. Bu, "şu etiketlerden herhangi birine sahip olanlar" türündeki sorgular için doğal çözümdür. Operatörlerin genel davranışı için sorgu operatörleri yazısına bakabilirsiniz.

$all: belirtilen elemanların hepsi

$all, alanın verilen değerlerin tamamını içerdiği belgeleri seçer; sıra ve ek elemanlar önemli değildir. Belgeye göre { etiketler: { $all: [ "ssl", "security" ] } } ifadesi, { $and: [ { etiketler: "ssl" }, { etiketler: "security" } ] } ile eşdeğerdir. $all ayrıca dizi olmayan bir alanı da eşleyebilir: alan tek bir değer taşıyorsa ve $all listesinde yalnızca o değer varsa belge seçilir.

javascript
// Hem "veritabani" hem "nosql" etiketli makaleler (sıra önemsiz)
db.makaleler.find( { etiketler: { $all: [ "nosql", "veritabani" ] } } )   // _id 1

İki uç durum belgede ayrıca belirtilir. Birincisi: $all'a boş bir dizi verirseniz hiçbir belge eşleşmez. İkincisi: iç içe bir dizi verirseniz ($all: [ [ "A" ] ]), sorgu ya o iç diziyi eleman olarak içeren ya da alanı tam o diziye eşit olan belgeleri eşler. Programatik olarak liste oluşturuyorsanız, boş liste durumunu uygulama tarafında ayrıca ele alın; aksi halde "filtre yok" beklerken "sonuç yok" alırsınız.

$size: dizi uzunluğu

$size, eleman sayısı tam olarak verilen sayıya eşit olan dizileri eşler. { etiketler: { $size: 2 } } 1 ve 3 numaralı makaleleri, { etiketler: { $size: 0 } } ise boş etiketli 4 numarayı getirir. Dizi olmayan bir alan $size ile eşleşmez.

javascript
db.makaleler.find( { etiketler: { $size: 2 } } )   // _id 1 ve 3
db.makaleler.find( { etiketler: { $size: 0 } } )   // _id 4
javascript
// Sayaç alanı yaklaşımı: eleman eklerken sayacı da artırın
db.makaleler.updateOne(
   { _id: 2 },
   { $push: { etiketler: "rehber" }, $inc: { etiketSayisi: 1 } }
)
db.makaleler.find( { etiketSayisi: { $gt: 3 } } )

Belirli bir konumdaki eleman

Dizide belirli bir konuma koşul yazmak için nokta gösterimiyle sıfırdan başlayan indeks numarası kullanılır. Belgede belirtildiği gibi bu durumda alan adı ve indeks tırnak içinde yazılmalıdır. { "etiketler.0": "veritabani" } ilk etiketi "veritabani" olan makaleleri (1 ve 3) getirir.

javascript
db.makaleler.find( { "etiketler.0": "veritabani" } )    // ilk eleman
db.depo.find( { "stok.0.adet": { $lte: 20 } } )       // ilk stok kaydının adedi 20 veya altı

Birden çok koşul: farklı elemanlar mı, aynı eleman mı?

Birden çok koşul: farklı elemanlar mı, aynı eleman mı? { puanlar: { $gt: 80, $lt: 85 } }, { puanlar: { $elemMatch: { $gte: 80, $lt: 85 } } }, { etiketler: { $all: [ "a", "b" ] }...

Bu yazının en önemli bölümü budur. Bir dizi alanına $elemMatch olmadan birden çok koşul yazdığınızda, belge koşulların herhangi bir eleman kombinasyonu tarafından sağlanmasını yeterli sayar. Bir eleman ilk koşulu, başka bir eleman ikinci koşulu karşılayabilir. $elemMatch kullandığınızda ise en az bir elemanın bütün koşulları tek başına sağlaması gerekir.

javascript
// Kombinasyon: bir puan 80'den büyük, (belki başka) bir puan 85'ten küçük olsun
db.makaleler.find( { puanlar: { $gt: 80, $lt: 85 } } )

// Aynı eleman: tek bir puan hem 80 veya üstü hem 85'ten küçük olsun
db.makaleler.find( { puanlar: { $elemMatch: { $gte: 80, $lt: 85 } } } )

Örnek veride ilk sorgu 1 ve 2 numaralı makaleleri getirir. 2 numaralı makalenin hiçbir puanı 80 ile 85 arasında değildir, ama 88 değeri "80'den büyük" koşulunu, 75 değeri ise "85'ten küçük" koşulunu ayrı ayrı sağlar; kombinasyon yeterli olduğu için belge eşleşir. İkinci sorgu ise yalnızca 1 numarayı getirir, çünkü orada 82 değeri iki koşulu birlikte karşılar. Belgedeki scores örneği de aynı farkı gösterir: { results: { $elemMatch: { $gte: 80, $lt: 85 } } } sorgusu yalnızca 82 değerini içeren belgeyi döndürür.

FiltreAnlamıBu verideki sonuç
{ puanlar: { $gt: 80, $lt: 85 } }Koşullar farklı elemanlarca sağlanabilir_id 1 ve 2
{ puanlar: { $elemMatch: { $gte: 80, $lt: 85 } } }Tek eleman iki koşulu birlikte sağlar_id 1
{ etiketler: { $all: [ "a", "b" ] } }Her iki değer de dizide varSıra önemsiz
{ etiketler: [ "a", "b" ] }Dizi tam olarak buSıra ve uzunluk önemli

Gömülü belge dizilerini sorgulamak

Gerçek uygulamalarda diziler çoğu zaman belge içerir: sipariş kalemleri, adresler, depo stokları. Bu dizilerde de aynı iki yol vardır. Nokta gösterimiyle "stok.adet" yazdığınızda MongoDB dizinin her belgesindeki adet alanına bakar ve en az birinin koşulu sağlamasını arar. { "stok.adet": { $lte: 20 } } defter, kalem, kağıt ve ajanda belgelerinin dördünü de getirir, çünkü her birinde adedi 20 veya altında olan en az bir kayıt vardır.

javascript
db.depo.find( { "stok.adet": { $lte: 20 } } )

Dizideki bir belgeyle birebir eşleşme

Bir dizi elemanını bütün belge olarak eşleyebilirsiniz: { stok: { depo: "A", adet: 5 } }. Ancak gömülü belgelerde olduğu gibi burada da alan sırası dahil birebir eşleşme gerekir; { adet: 5, depo: "A" } yazarsanız hiçbir şey dönmez. Bu yüzden birden çok alanlı koşulları $elemMatch ile yazmak daha güvenlidir.

javascript
db.depo.find( { stok: { depo: "A", adet: 5 } } )   // defter
db.depo.find( { stok: { adet: 5, depo: "A" } } )   // eşleşme yok: alan sırası farklı

$elemMatch ile aynı kayıtta iki koşul

"A deposunda 5 adet olan" ürünü arıyorsanız iki koşulun aynı stok kaydında sağlanması gerekir. $elemMatch ile yazılan sorgu yalnızca defteri getirir. Nokta gösterimiyle ayrı ayrı yazılan { "stok.adet": 5, "stok.depo": "A" } sorgusu ise ajandayı da getirir: ajandanın A kaydında adet 40, B kaydında adet 5 olduğu için koşullar iki farklı kayıtta karşılanır. Raporlarda yanlış sayıların kaynağı genellikle bu farktır.

javascript
// Aynı kayıt: depo A ve adet 5
db.depo.find( { stok: { $elemMatch: { depo: "A", adet: 5 } } } )   // defter

// Farklı kayıtlar da olabilir
db.depo.find( { "stok.adet": 5, "stok.depo": "A" } )               // defter ve ajanda

// Aynı kayıtta aralık: adet 10'dan büyük ve 20 veya altı
db.depo.find( { stok: { $elemMatch: { adet: { $gt: 10, $lte: 20 } } } } )   // defter ve kağıt

Tek koşullu $elemMatch da anlamlıdır. Belgedeki survey örneği bunu gösterir: results alanı bazı belgelerde dizi, birinde ise tek bir belgedir. { results: { $elemMatch: { product: "xyz" } } } yalnızca dizi elemanlarına bakar ve dizi olmayan belgeyi döndürmez; { "results.product": "xyz" } ise o belgeyi de döndürür. Negatif koşullarda fark daha da belirgindir: $elemMatch içindeki { product: { $ne: "xyz" } } "xyz olmayan en az bir eleman" anlamına gelirken, nokta gösterimiyle yazılan { "results.product": { $ne: "xyz" } } "hiçbir elemanı xyz olmayan" belgeleri seçer.

Dizi elemanlarını seçerek döndürmek (projection)

Sorgu bir belgeyi eşlediğinde varsayılan olarak dizinin tamamı döner. Yalnızca bazı elemanları döndürmek için üç projection operatörü vardır: $elemMatch, $slice ve konumsal $. Belgeye göre dizi elemanlarını seçmenin yolu yalnızca bunlardır; { "stok.0": 1 } gibi indeksle projection ilk elemanı döndürmez.

javascript
// $slice: ilk 2 eleman, son 1 eleman, 1 atla 2 al
db.makaleler.find( {}, { baslik: 1, puanlar: { $slice: 2 } } )
db.makaleler.find( {}, { baslik: 1, puanlar: { $slice: -1 } } )
db.makaleler.find( {}, { baslik: 1, puanlar: { $slice: [ 1, 2 ] } } )

// Konumsal $: sorgu koşulunu sağlayan ilk eleman
db.depo.find( { "stok.depo": "B" }, { urun: 1, "stok.$": 1 } )

// $elemMatch projection: kendi koşuluyla ilk eşleşen eleman
db.depo.find( {}, { urun: 1, stok: { $elemMatch: { adet: { $gte: 40 } } } } )

Konumsal $ ile $elemMatch projection'ı ikisi de ilk eşleşen elemanı döndürür. Fark, $'ın koşulu sorgu filtresinden alması, $elemMatch'in ise kendi açık koşulunu taşımasıdır. Bu sayede sorguda olmayan bir koşula göre ya da gömülü belgenin birden çok alanına göre seçim yapabilirsiniz. Konumsal $ için belge bazı kısıtlar koyar: projection belgesinde yalnızca bir $ bulunabilir, sorgu belgesinde yalnızca bir dizi alanı olmalıdır ve o dizi alanı için tek bir koşul yazılmalıdır; aksi halde davranış tanımsızdır.

$slice için kurallar şöyledir: pozitif sayı baştan, negatif sayı sondan o kadar eleman döndürür; sayı eleman sayısından büyükse dizinin tamamı döner. [ atla, al ] biçiminde iki sayı verirseniz önce belirtilen kadar eleman atlanır, sonra istenen kadar eleman döner; burada "al" değeri pozitif olmalıdır. $slice ile aynı dizinin bir iç alanını aynı projection içinde kullanmak Path collision hatası verir. Hepsini döndürmek yerine koşula uyan tüm elemanlara ihtiyacınız varsa aggregation'daki $filter ifadesine başvurun; bu konu $addFields ve $project yazısında.

Diziler ve null

Bir alan dizi olduğunda MongoDB sorgu değerini hem her elemanla hem de dizinin kendisiyle karşılaştırır. Bu yüzden { a: null } sorgusu, alan null olduğunda, alan hiç olmadığında ve dizi en az bir null eleman içerdiğinde eşleşir; boş dizi ve null içermeyen dizi eşleşmez. { "a.b": null } gibi bir dizinin içinden geçen nokta gösterimi ise dizideki belgelerden birinde b eksikse de eşleşir. Belgedeki ilgili bölüm, { $ne: null } filtresinin dizi içinden geçen yollarda $exists ile aynı sonucu vermediğini de vurgular. Null sorgularının genel kuralları find sorguları yazısında.

Dizi alanlarında indeks

Dizi içeren bir alanda indeks oluşturduğunuzda MongoDB bu indeksi otomatik olarak multikey indeks yapar; ayrıca belirtmeniz gerekmez. Multikey indekste tek bir belge, dizideki her eleman için ayrı bir indeks girdisine sahip olabilir. etiketler veya stok.depo gibi sık filtrelediğiniz dizi alanları için indeks, eleman aramalarını ve $elemMatch sorgularını hızlandırır. İndeks oluşturma ayrıntıları için index oluşturma yazısına bakın.

javascript
db.makaleler.createIndex( { etiketler: 1 } )
db.depo.createIndex( { "stok.depo": 1, "stok.adet": 1 } )

Diziler sınırsız büyümemelidir. Bir belgenin en fazla boyutu 16 mebibayttır ve sürekli büyüyen bir dizi (örneğin her yorumu tek bir gönderi belgesine eklemek) hem bu sınıra yaklaşır hem de indeksleri şişirir. Diziyi ne zaman gömüp ne zaman ayrı koleksiyona taşıyacağınızı veri modelleme yazısında tartışıyoruz.

Uygulamalı mini örnek: etiket filtresi

Bir blog arayüzünde kullanıcı etiket seçiyor ve ayrıca "en az bir puanı 85 veya üstü olsun" filtresi uyguluyor. Seçilen etiketlerin hepsini taşıyan makaleler için $all, herhangi birini taşıyanlar için $in kullanırız. Liste boş geldiğinde $all hiçbir belgeyi eşlemeyeceği için filtreyi koşullu olarak ekliyoruz.

javascript
const secilen = [ "nosql" ];   // arayüzden gelen etiketler
const filtre = { puanlar: { $gte: 85 } };
if ( secilen.length > 0 ) {
   filtre.etiketler = { $all: secilen };
}
db.makaleler.find( filtre, { _id: 0, baslik: 1, puanlar: { $slice: -1 } } )

Örnek veride bu sorgu 1 ve 2 numaralı makaleleri eşler; ikisinin de "nosql" etiketi ve 85 veya üzeri en az bir puanı vardır. Projection, her makalenin yalnızca son puanını döndürür.

Sık yapılan hatalar

Sık yapılan hatalar: Aynı eleman koşulunu $elemMatch olmadan yazmak, Sırası önemsiz bir eşleşme için dizi eşitliği kullanmak, $size ile aralık sorgusu denemek, Boş listeyle $all...
  • Aynı eleman koşulunu $elemMatch olmadan yazmak: { "stok.depo": "A", "stok.adet": 5 } koşulları farklı kayıtlarda karşılanabilir.
  • Sırası önemsiz bir eşleşme için dizi eşitliği kullanmak: { etiketler: [ "a", "b" ] } sırayı ve uzunluğu da kontrol eder; $all kullanın.
  • $size ile aralık sorgusu denemek: desteklenmez; sayaç alanı tutun.
  • Boş listeyle $all göndermek: hiçbir belge eşleşmez; boş listeyi uygulamada ele alın.
  • Gömülü dizi belgesini alan sırası farklı yazmak: birebir belge eşleşmesi sıraya duyarlıdır.
  • Indeks numarasıyla projection yapmak: { "dizi.0": 1 } beklediğiniz elemanı döndürmez; $slice kullanın.
  • Diziyi sınırsız büyütmek: 16 mebibaytlık belge sınırına ve büyük indekslere yol açar.

Sık Sorulan Sorular

$elemMatch ne zaman gereksizdir?

Skaler bir diziye tek bir koşul yazıyorsanız ({ etiketler: "nosql" } veya { puanlar: { $gt: 90 } }) $elemMatch gerekmez; koşul zaten elemanlara tek tek uygulanır. Birden çok koşulun aynı elemanda sağlanması gerektiğinde ya da dizi olmayan değerleri dışarıda bırakmak istediğinizde gereklidir.

$all ile $in arasındaki fark nedir?

$all listedeki değerlerin hepsinin dizide bulunmasını, $in ise en az birinin bulunmasını ister. Biri AND, diğeri OR mantığıdır.

Dizi elemanını güncellemek için hangi operatörü kullanırım?

Sorgudaki koşulu sağlayan ilk elemanı güncellemek için konumsal $, tüm elemanlar için $[], belirli koşula uyan elemanlar için $[<identifier>] ve arrayFilters kullanılır. Ayrıntılar dizi güncelleme yazısında.

Aggregation içinde diziyi satırlara açabilir miyim?

Evet, $unwind aşaması dizinin her elemanı için ayrı bir belge üretir. Bu aşama $lookup ve $unwind yazısında anlatılıyor.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: MongoDB öğrenme rehberi · Dizi güncelleme: $push ve $pull · find sorguları · Sorgu operatörleri · Veri modelleme · Index oluşturma