MongoDB ile çalışmaya başladığınızda, ilişkisel veritabanlarından gelen alışkanlıklarınızı bir kenara bırakmanız gerekiyor. “Bu veriyi ayrı bir koleksiyona mı koyayım, yoksa doğrudan dökümanın içine gömmeli miyim?” sorusu, MongoDB tasarımında verilen en kritik kararlardan biri. Yanlış seçim, ilerleyen süreçte performans kabuslarına, karmaşık sorgulara ve ciddi ölçekleme sorunlarına yol açıyor. Bu yazıda embedding ve referencing yaklaşımlarını gerçek dünya senaryolarıyla ele alacağız.
Temel Kavramlar: Ne Demek Bunlar?
Önce terminolojiyi netleştirelim.
Embedding (Gömme), ilişkili verileri tek bir döküman içinde iç içe saklama yöntemidir. Bir blog yazısı ve onun yorumları varsa, yorumları doğrudan blog yazısı dökümanının içine bir dizi olarak koyarsınız.
Referencing (Referans Verme) ise ilişkisel veritabanındaki foreign key mantığına benzer. İlişkili veriler ayrı koleksiyonlarda tutulur, birbirlerine ObjectId veya benzeri bir değer aracılığıyla bağlanır.
MongoDB resmi dokümantasyonu bu iki yaklaşımı “denormalization” ve “normalization” olarak da tanımlar. Ancak MongoDB’nin doküman odaklı yapısı, geleneksel normalizasyon kurallarını doğrudan uygulamayı zorlaştırır. Kararlarınızı veri erişim kalıplarına göre vermeniz gerekir.
Embedding: Ne Zaman Kullanmalı?
Embedding’in en güçlü olduğu senaryolar genellikle şu özellikleri taşır: veriler birlikte okunur, veriler birlikte yazılır ve alt veri seti bağımsız anlam taşımaz.
Kullanıcı Profili ve Adresleri
Bir e-ticaret uygulaması düşünelim. Kullanıcıların birden fazla adresi olabilir. Bu adresler her zaman kullanıcı bilgisiyle birlikte yüklenir, bağımsız olarak sorgulanmaz.
db.users.insertOne({
_id: ObjectId("64a1b2c3d4e5f6a7b8c9d0e1"),
name: "Ahmet Yılmaz",
email: "[email protected]",
addresses: [
{
type: "home",
street: "Atatürk Cad. No:15",
city: "İstanbul",
district: "Kadıköy",
zip: "34710",
isDefault: true
},
{
type: "work",
street: "Levent Plaza Kat:5",
city: "İstanbul",
district: "Beşiktaş",
zip: "34340",
isDefault: false
}
],
createdAt: ISODate("2024-01-15T10:30:00Z")
})
Bu tasarımda kullanıcıyı çektiğinizde adreslere de anında erişirsiniz. Ayrı bir addresses koleksiyonu açıp her profil yüklemesinde join benzeri işlem yapmanıza gerek yoktur.
Ürün Özellikleri ve Varyantları
Bir ürünün teknik özellikleri genellikle o ürünle birlikte görüntülenir. Televizyon sayfasını açtığınızda ekran boyutu, çözünürlük, bağlantı seçenekleri hep birlikte gelir.
db.products.insertOne({
_id: ObjectId("64b2c3d4e5f6a7b8c9d0e2f3"),
sku: "TV-SAMSUNG-65-4K",
name: "Samsung 65 inç 4K QLED TV",
price: 45000,
stock: 23,
specs: {
screenSize: "65 inch",
resolution: "3840x2160",
refreshRate: "120Hz",
hdrSupport: ["HDR10+", "HLG"],
connectivity: {
hdmiPorts: 4,
usbPorts: 2,
bluetooth: "5.0",
wifi: "802.11ac"
}
},
variants: [
{ color: "Siyah", sku: "TV-SAMSUNG-65-4K-BLK", stock: 15 },
{ color: "Gümüş", sku: "TV-SAMSUNG-65-4K-SLV", stock: 8 }
]
})
Burada specs ve variants nesneleri her zaman ürünle birlikte işlenir. Ayrıştırmanın hiçbir anlamı yoktur.
Embedding’in Sınırları
MongoDB’de tek bir döküman boyutu 16 MB ile sınırlıdır. Bu sınıra takılmak çok da zor değil aslında, özellikle sürekli büyüyen veriler söz konusu olduğunda.
Bir forum uygulamasında tüm yorumları ana konunun içine gömerseniz, popüler bir konu yüzlerce yorumla dolup taşabilir. Ayrıca bir yorumu güncellemek için tüm dökümanı yeniden yazmanız gerekir, bu da performans sorunlarına yol açar.
Referencing: Ne Zaman Kullanmalı?
Referencing, veriler bağımsız olarak sorgulanabildiğinde, ilişkiler çok-çok olduğunda veya alt veriler çok hızlı büyüdüğünde devreye girer.
Siparişler ve Ürünler İlişkisi
Bir sipariş ile ürünler arasındaki ilişki klasik bir referencing örneğidir. Ürün bilgileri değişebilir (fiyat güncellenir, stok düşer), siparişlerin bağımsız yönetilmesi gerekir.
# Ürün koleksiyonu
db.products.insertOne({
_id: ObjectId("64c3d4e5f6a7b8c9d0e3f4a5"),
name: "Logitech MX Master 3 Mouse",
price: 2800,
category: "peripherals"
})
# Sipariş koleksiyonu - referans ile
db.orders.insertOne({
_id: ObjectId("64d4e5f6a7b8c9d0e4f5a6b7"),
userId: ObjectId("64a1b2c3d4e5f6a7b8c9d0e1"),
orderDate: ISODate("2024-06-15T14:22:00Z"),
status: "shipped",
items: [
{
productId: ObjectId("64c3d4e5f6a7b8c9d0e3f4a5"),
quantity: 2,
unitPrice: 2800,
productName: "Logitech MX Master 3 Mouse"
}
],
totalAmount: 5600,
shippingAddress: {
street: "Atatürk Cad. No:15",
city: "İstanbul"
}
})
Dikkat edin, sipariş kaydında productName ve unitPrice değerlerini de sakladım. Bu hibrit yaklaşım çok önemlidir. Ürün fiyatı ileride değişse bile sipariş o anki fiyatı korumalıdır. Referans ID’si ile ürünün güncel bilgilerine ulaşabilir, ancak sipariş tarihindeki kritik veriler gömülü olarak kalır.
$lookup ile Referans Sorgulama
Referencing kullandığınızda verileri bir araya getirmek için $lookup aggregation stage’ini kullanırsınız. Bu, SQL’deki JOIN’in MongoDB karşılığıdır.
db.orders.aggregate([
{
$match: {
userId: ObjectId("64a1b2c3d4e5f6a7b8c9d0e1"),
status: "shipped"
}
},
{
$lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "customerInfo"
}
},
{
$unwind: "$customerInfo"
},
{
$project: {
orderDate: 1,
totalAmount: 1,
"customerInfo.name": 1,
"customerInfo.email": 1,
items: 1
}
}
])
$lookup güçlüdür ama bedavaya gelmez. Her aggregation pipeline çalıştırdığınızda ek I/O işlemi gerçekleşir. Sık çalışan ve yüksek trafikli sorgularda bu maliyeti göz önünde bulundurmanız şarttır.
Yazar ve Makaleler: Çok-Çok İlişki
Bir teknik blog platformunda yazarlar birden fazla makalede katkıda bulunabilir, makaleler birden fazla yazara sahip olabilir. Bu klasik çok-çok ilişki referencing için ideal bir senaryodur.
# Yazar dökümanı
db.authors.insertMany([
{
_id: ObjectId("64e5f6a7b8c9d0e5f6a7b8c9"),
name: "Mehmet Kaya",
bio: "15 yıllık Linux sistem yöneticisi",
expertise: ["Linux", "Kubernetes", "MongoDB"]
},
{
_id: ObjectId("64f6a7b8c9d0e6f7a8b9c0d1"),
name: "Zeynep Arslan",
bio: "DevOps mühendisi ve veritabanı uzmanı",
expertise: ["MongoDB", "PostgreSQL", "Terraform"]
}
])
# Makale dökümanı - çoklu yazar referansı
db.articles.insertOne({
_id: ObjectId("6507a8b9c0d1e2f3a4b5c6d7"),
title: "MongoDB Replica Set Kurulumu",
slug: "mongodb-replica-set-kurulumu",
authorIds: [
ObjectId("64e5f6a7b8c9d0e5f6a7b8c9"),
ObjectId("64f6a7b8c9d0e6f7a8b9c0d1")
],
publishedAt: ISODate("2024-06-20T09:00:00Z"),
tags: ["mongodb", "replication", "high-availability"],
content: "..."
})
Hibrit Yaklaşım: Gerçek Dünya Çoğunlukla Ortada
Prodüksiyon sistemlerinde genellikle “ya embedding ya referencing” gibi keskin bir seçim yapmak yerine ikisini birden kullanırsınız. Önemli olan hangi verinin nerede olması gerektiğini doğru belirlemektir.
Sosyal Medya Gönderisi Tasarımı
Bir sosyal medya platformu düşünelim. Gönderi, beğeniler, yorumlar ve medya dosyaları var.
db.posts.insertOne({
_id: ObjectId("6508b9c0d1e2f3a4b5c6d7e8"),
authorId: ObjectId("64e5f6a7b8c9d0e5f6a7b8c9"),
# Yazar önizleme bilgileri gömülü (performans için)
authorSnapshot: {
name: "Mehmet Kaya",
avatarUrl: "/avatars/mehmet-kaya.jpg",
username: "mehmetkaya"
},
content: "MongoDB şema tasarımı hakkında yeni yazım yayında!",
# Medya referansla ayrı koleksiyonda
mediaIds: [
ObjectId("6509c0d1e2f3a4b5c6d7e8f9")
],
# Beğeni sayısı gömülü (counter denormalizasyonu)
likeCount: 247,
commentCount: 38,
# Son 3 yorum gömülü (hız için), tamamı ayrı koleksiyonda
recentComments: [
{
_id: ObjectId("650ac0d1e2f3a4b5c6d7e8f9"),
authorName: "Zeynep Arslan",
content: "Harika yazı, teşekkürler!",
createdAt: ISODate("2024-06-21T11:15:00Z")
}
],
createdAt: ISODate("2024-06-21T10:00:00Z"),
isPublished: true
})
Bu tasarımda ne yapıldığına dikkat edin:
- authorSnapshot gömüldü çünkü her gönderi yüklemesinde yazar adı ve avatarı gösterilmeli. Her seferinde users koleksiyonuna gitmek gereksiz yük olurdu.
- mediaIds referans olarak tutuldu çünkü medya dosyaları büyük, ayrı yönetilmesi gerekiyor.
- likeCount ve commentCount denormalize sayaçlar olarak tutuldu, sorgulama hızını artırıyor.
- recentComments ilk 3 yorum gömüldü, sayfa açılışında ekstra sorgu gerekmez.
Index Stratejisi: Referencing ile Performans
Referencing kullandığınızda doğru indexleme hayati önem taşır. Yoksa $lookup sorguları tüm koleksiyonu taramak zorunda kalır.
# userId alanına index ekle
db.orders.createIndex({ userId: 1 })
# Composite index - çok kullanılan sorgu kalıbına göre
db.orders.createIndex({ userId: 1, status: 1, orderDate: -1 })
# Referans alanı üzerine sparse index
db.articles.createIndex(
{ authorIds: 1 },
{ sparse: true }
)
# Index kullanımını doğrula
db.orders.find(
{ userId: ObjectId("64a1b2c3d4e5f6a7b8c9d0e1") }
).explain("executionStats")
explain("executionStats") çıktısında IXSCAN görüyorsanız index kullanılıyor demektir. COLLSCAN görüyorsanız indexinizi gözden geçirin.
Büyüyen Array Problemi ve Çözümü
Embedding kullanırken en sık karşılaşılan problem, dizilerin zamanla kontrolsüz büyümesidir. Bir haber sitesinde her haberin yorumlarını gömülü tutarsanız ve o haber viral olursa ne olur?
# KOTU YAKLASIM - sürekli büyüyen yorum dizisi
db.news.updateOne(
{ _id: ObjectId("6508b9c0d1e2f3a4b5c6d7e8") },
{
$push: {
comments: {
author: "Ali Veli",
content: "Bu haber çok önemli!",
createdAt: new Date()
}
}
}
)
# Haber çok popüler olursa döküman 16MB limitine takılır
# Her yorum eklenmesinde tüm döküman yeniden yazılır
# DAHA IYI YAKLASIM - yorumlar ayrı koleksiyonda
db.comments.insertOne({
_id: ObjectId("650bc1d2e3f4a5b6c7d8e9fa"),
newsId: ObjectId("6508b9c0d1e2f3a4b5c6d7e8"),
author: "Ali Veli",
content: "Bu haber çok önemli!",
createdAt: ISODate("2024-06-22T08:45:00Z"),
likes: 12
})
# Haber dökümanında sadece sayaç tutulur
db.news.updateOne(
{ _id: ObjectId("6508b9c0d1e2f3a4b5c6d7e8") },
{ $inc: { commentCount: 1 } }
)
Migrasyon Senaryosu: Embedding’den Referencing’e Geçiş
Başlangıçta embedding yaptınız, sistem büyüdü ve artık referencing’e geçmeniz gerekiyor. Bu gerçek dünya senaryosunda nasıl bir yol izlersiniz?
# Mevcut blog yazıları koleksiyonunu incele
db.blogPosts.find({
"comments.10": { $exists: true } # 10'dan fazla yorumu olanları bul
}).count()
# Migrasyon scripti - gömülü yorumları ayrı koleksiyona taşı
db.blogPosts.find({}).forEach(function(post) {
if (post.comments && post.comments.length > 0) {
# Her yorum için yeni döküman oluştur
post.comments.forEach(function(comment) {
db.comments.insertOne({
postId: post._id,
author: comment.author,
content: comment.content,
createdAt: comment.createdAt || new Date()
});
});
# Blog yazısından yorumları kaldır, sayaç ekle
db.blogPosts.updateOne(
{ _id: post._id },
{
$unset: { comments: "" },
$set: { commentCount: post.comments.length }
}
);
}
});
# Migrasyon sonrası doğrulama
var postCount = db.blogPosts.countDocuments({ comments: { $exists: true } });
print("Migrasyon tamamlandı. Kalan gömülü yorum: " + postCount);
Bunu üretim ortamında yaparken dikkatli olun. Mutlaka backup alın, batch işlemlerle yapın ve uygulama tarafında özellik bayrağı (feature flag) kullanarak kademeli geçiş yapın.
Karar Verme Kriterleri
Hangi yaklaşımı seçeceğinize karar verirken şu soruları kendinize sorun:
Embedding lehine kararlar:
- Veri her zaman üst dökümanla birlikte mi okunuyor?
- Alt döküman sayısı sınırlı mı ve büyümüyor mu?
- Atomik yazma işlemi gerekiyor mu?
- Alt veri bağımsız anlam taşımıyor mu?
Referencing lehine kararlar:
- Alt veri bağımsız olarak sorgulanıyor mu?
- İlişki çok-çok mı?
- Alt döküman sayısı sınırsız büyüyebilir mi?
- Birden fazla üst döküman aynı alt veriye referans veriyor mu?
- Alt veri çok sık güncellenip üst dökümanın geri kalanı okunmadan yazılıyor mu?
Her iki durumda da dikkat edilmesi gerekenler:
- 16 MB döküman limiti: Embedding ile büyüyebilecek veriler için dikkatli olun.
- Atomicity: MongoDB 4.0+ ile çok belgeli işlemler için transaction kullanabilirsiniz ama bu ek karmaşıklık demektir.
- Read/Write oranı: Çok okunan ama az yazılan veri için embedding, sık güncellenen veri için referencing daha uygun olabilir.
Performans Testi Yapın
Teorik kararlarınızı her zaman gerçek veriyle test edin. Özellikle $lookup sorgularının performansını ölçmek için MongoDB’nin profiler’ını kullanabilirsiniz.
# Yavaş sorguları loglamak için profiler'ı aç
db.setProfilingLevel(1, { slowms: 100 })
# Profiler çıktısını incele
db.system.profile.find(
{ op: "command", "command.aggregate": { $exists: true } },
{ ns: 1, millis: 1, "command.pipeline": 1 }
).sort({ millis: -1 }).limit(10)
# Belirli bir sorgunun execution planını incele
db.orders.aggregate([
{ $match: { userId: ObjectId("64a1b2c3d4e5f6a7b8c9d0e1") } },
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}}
]).explain("executionStats")
Profiler’ı sürekli açık bırakmayın, system.profile koleksiyonu disk doldurmaya başlar. Sadece sorun giderme veya performans analizi sırasında açın, işiniz bitince kapatın.
# Profiler'ı kapat
db.setProfilingLevel(0)
Sonuç
MongoDB’de embedding ve referencing arasındaki seçim, “doğru” veya “yanlış” cevabı olmayan, bağlama göre değişen bir mimari karardır. İlişkisel veritabanı dünyasından gelen “her zaman normalize et” veya “join’den kaç” gibi katı kurallar burada işe yaramaz.
Pratikte en iyi sonuçları genellikle hibrit yaklaşım verir. Birlikte okunan, küçük ve sabit yapıdaki verileri gömerken, bağımsız sorgulanması gereken, büyüyebilecek veya çok-çok ilişkili verileri referansla tutarsınız. Sonra gerçek yük altında profiler ile ölçer, gerekirse tasarımı revize edersiniz.
En kritik tavsiyem şu: Uygulamanızın veri erişim kalıplarını önce kağıda dökün. “Bu veri hangi ekranda, hangi veriyle birlikte gösterilecek?” sorusunun cevabı çoğunlukla size doğru yönü gösterir. Schema tasarımını uygulama ihtiyaçlarına göre yapmak, MongoDB’nin sunduğu esnekliği gerçek anlamda kullanmak demektir.