MongoDB, verileri satır ve sütunlardan oluşan tablolar yerine esnek, JSON benzeri dokümanlar halinde saklayan, doküman tabanlı (document-oriented) bir NoSQL veritabanıdır. Şemayı uygulamanızla birlikte büyütebilmeniz, JavaScript ekosistemiyle doğal uyumu ve yatay ölçeklenebilirliği sayesinde küçük kişisel projelerden milyonlarca kullanıcılı sistemlere kadar çok geniş bir yelpazede kullanılır.
Bu rehberde MongoDB'yi sıfırdan ele alacağız: NoSQL kavramından BSON veri tiplerine, kurulumdan bağlantı dizelerine, CRUD işlemlerinden güncelleme ve sorgu operatörlerine, indekslerden aggregation pipeline'a, veri modellemeden şema doğrulamaya, Node.js, Next.js ve Mongoose entegrasyonundan transaction, replikasyon, güvenlik ve yedeklemeye kadar ihtiyacınız olan her konuyu örneklerle inceleyeceğiz. Yazının sonunda tüm parçaları birleştiren eksiksiz bir blog veritabanı örneği ve hızlı başvuru listesi de bulacaksınız.
MongoDB Nedir?
MongoDB, verileri BSON (Binary JSON) formatındaki dokümanlarda saklar. Her doküman, alan–değer (field–value) çiftlerinden oluşan bir nesnedir. Değerler metin ve sayı olabileceği gibi bir dizi ya da iç içe başka bir doküman da olabilir. Bu sayede uygulamanızdaki bir nesneyi neredeyse olduğu gibi veritabanına yazabilirsiniz:
{
"_id": ObjectId("651f3c2e8f1b2c3a4d5e6f7a"),
"name": "Ertuğrul Mert",
"role": "Full Stack Developer",
"skills": ["Node.js", "MongoDB", "Next.js"],
"social": {
"github": "ertucuk",
"website": "https://portfolio.ertucuk.com"
},
"createdAt": ISODate("2026-02-18T09:00:00Z")
}
İlişkisel bir veritabanında bu veriyi en az üç tabloya (kullanıcılar, yetenekler, sosyal hesaplar) bölüp sorgu sırasında JOIN ile yeniden birleştirmeniz gerekirdi. MongoDB'de ise birlikte okunan veriler birlikte saklanır; profili tek bir okuma işlemiyle, tek parça halinde alırsınız.
NoSQL Ne Demek?
NoSQL, "Not only SQL" (yalnızca SQL değil) anlamına gelir ve tablo dışındaki veri modellerini kullanan veritabanlarının ortak adıdır. SQL'e karşı olmak değil, farklı veri biçimleri için farklı araçlar kullanmak demektir. Dört ana ailesi vardır:
- Doküman tabanlı: Veriyi JSON benzeri dokümanlarda tutar. MongoDB, Couchbase, Firestore.
- Anahtar–değer: Her kayıt bir anahtar ve ona bağlı bir değerden oluşur; çok hızlıdır. Redis, DynamoDB.
- Geniş sütunlu (wide-column): Devasa ölçekte yazma için sütun aileleri kullanır. Cassandra, HBase.
- Graf: Düğümler ve aralarındaki ilişkiler üzerine kuruludur; sosyal ağlar ve öneri sistemleri için idealdir. Neo4j.
MongoDB, doküman tabanlı ailenin en yaygın kullanılan üyesidir.
Kısa Bir Tarihçe
- 2007: Dwight Merriman, Eliot Horowitz ve Kevin Ryan tarafından kurulan 10gen şirketi, MongoDB'yi geliştirmeye başlar.
- 2009: MongoDB açık kaynak olarak yayımlanır. Adı, "devasa" anlamına gelen humongous kelimesinden gelir.
- 2013: Şirket, adını MongoDB Inc. olarak değiştirir.
- 2016: Tam yönetilen bulut veritabanı hizmeti MongoDB Atlas kullanıma sunulur.
- 2018: Sürüm 4.0 ile birden fazla dokümanı kapsayan ACID transaction desteği gelir; lisans SSPL'e geçer.
- 2021: Sürüm 5.0 ile zaman serisi (time series) koleksiyonları eklenir.
- 2023: Sürüm 7.0 ile verileri şifreliyken sorgulamayı sağlayan Queryable Encryption genel kullanıma açılır.
- 2024: Sürüm 8.0, okuma ve yazma tarafında belirgin performans iyileştirmeleriyle yayımlanır.
İlişkisel Veritabanı mı, MongoDB mi?
MySQL ve PostgreSQL gibi ilişkisel veritabanları verileri önceden tanımlanmış tablolarda tutar ve ilişkileri yabancı anahtarlarla (foreign key) kurar. MongoDB ise şemayı uygulamaya bırakır ve ilişkili verileri çoğu zaman aynı dokümanda saklar. İki yaklaşım da güçlüdür; hangisinin seçileceği verinin doğasına ve nasıl okunduğuna bağlıdır.
Terim Karşılıkları
| İlişkisel (SQL) | MongoDB | Not |
|---|---|---|
| Veritabanı | Veritabanı | Koleksiyonları barındırır |
| Tablo | Koleksiyon (collection) | Önceden şema tanımı gerekmez |
| Satır | Doküman (document) | Her doküman farklı alanlara sahip olabilir |
| Sütun | Alan (field) | Değeri dizi veya iç içe doküman olabilir |
| Birincil anahtar | _id | Her dokümanda zorunlu, otomatik üretilir |
| JOIN | Gömme (embedding) veya $lookup | Çoğu zaman gömme tercih edilir |
| GROUP BY | Aggregation ($group) | Pipeline içinde adım adım işlenir |
| CREATE TABLE | Esnek şema + $jsonSchema | Doğrulama isteğe bağlıdır |
| Transaction | Transaction | Birden fazla doküman için 4.0'dan beri |
Aynı Sorgu, İki Dil
18 yaşından büyük geliştiricilerin adını ve rolünü, isme göre sıralı ilk 10 kayıt olarak getiren sorgu SQL'de şöyle yazılır:
SELECT name, role
FROM users
WHERE age > 18 AND role = 'Developer'
ORDER BY name
LIMIT 10;
Aynı sorgunun MongoDB karşılığı:
db.users
.find(
{ age: { $gt: 18 }, role: "Developer" }, // WHERE
{ name: 1, role: 1 } // SELECT
)
.sort({ name: 1 }) // ORDER BY
.limit(10); // LIMIT
Ne Zaman MongoDB Seçilmeli?
- Veri yapısı sık değişiyorsa: Yeni bir özellik eklerken migration yazmadan yeni alanlar ekleyebilirsiniz.
- Veri doğal olarak iç içeyse: Ürün katalogları, CMS içerikleri, kullanıcı profilleri, ayarlar ve sipariş detayları gibi.
- Yazma hacmi yüksekse: Log kayıtları, analitik olaylar ve IoT sensör verileri.
- JavaScript ağırlıklı bir yığın kullanıyorsanız: Node.js, Next.js ve Express ile veriyi neredeyse hiç dönüştürmeden kullanırsınız.
- Yatay ölçek gerekiyorsa: Sharding ile veriyi birden fazla sunucuya dağıtabilirsiniz.
Ne Zaman Başka Bir Şey Düşünülmeli?
- Verileriniz çok sayıda tabloyu birbirine bağlayan karmaşık ilişkisel raporlardan oluşuyorsa.
- Muhasebe gibi katı ilişkisel bütünlük (foreign key, cascade) kuralları sistemin merkezindeyse.
- Veri doğası gereği tablo şeklindeyse ve ekibiniz zaten SQL'de uzmansa.
İpucu: Doğru soru "hangisi daha iyi?" değil, "verim nasıl okunuyor ve yazılıyor?" sorusudur. Erişim kalıplarınızı (access patterns) bildiğinizde seçim kendiliğinden netleşir.
Neden MongoDB Kullanılır?
- Esnek şema: Dokümanlar farklı alanlara sahip olabilir; uygulama geliştikçe şema da kolayca değişir.
- Geliştirici deneyimi: Veriler JSON gibi göründüğü için kodunuzdaki nesnelerle veritabanındaki kayıtlar arasında dönüştürme katmanı ihtiyacı azalır.
- Güçlü sorgu dili: Filtreleme, dizi sorguları, metin araması, coğrafi sorgular ve aggregation pipeline tek bir dilde.
- Performans: İndeksler ve gömülü dokümanlar sayesinde ilişkili verilere tek sorguda erişilir.
- Yüksek erişilebilirlik: Replica set'ler sayesinde bir sunucu çökse bile sistem otomatik olarak çalışmaya devam eder.
- Ölçeklenebilirlik: Sharding ile veri ve yük birden çok sunucuya dağıtılır.
- Zengin ekosistem: Atlas, Compass, Atlas Search, Vector Search ve neredeyse her dil için resmi sürücüler.
- Ücretsiz başlangıç: Community Server ücretsizdir; Atlas'ın M0 kümesi 512 MB'a kadar ücretsizdir.
Temel Kavramlar
Database, Collection ve Document
MongoDB'de veriler üç seviyede organize edilir:
- Database (veritabanı): Koleksiyonları barındıran en üst düzey yapı. Genellikle her uygulamanın kendi veritabanı olur.
- Collection (koleksiyon): İlişkisel veritabanlarındaki tabloya benzer; aynı türdeki dokümanları barındırır.
- Document (doküman): BSON formatında saklanan, JSON'a benzer tek bir veri kaydı.
portfolio ← veritabanı
├── users ← koleksiyon
│ ├── { _id, name: "Ertuğrul Mert", role: "Developer" } ← doküman
│ └── { _id, name: "Utku Kösem", role: "Designer" }
└── projects
├── { _id, title: "Portfolyo", tags: ["Next.js"] }
└── { _id, title: "Badetekstil", tags: ["E-ticaret"] }
Veritabanı ve Koleksiyonlar Nasıl Oluşur?
MongoDB'de veritabanı ve koleksiyonları önceden oluşturmanız gerekmez. use komutuyla olmayan bir veritabanına geçebilir, ilk dokümanı eklediğiniz anda hem veritabanı hem koleksiyon otomatik oluşur. Özel ayarlar gerektiğinde koleksiyonu açıkça da oluşturabilirsiniz:
use portfolio // henüz diskte yok
db.users.insertOne({ name: "Ertu" }) // şimdi veritabanı ve koleksiyon oluştu
// Ayarlarla açıkça oluşturma: en fazla 1 MB tutan, en eskiyi silen "capped" koleksiyon
db.createCollection("logs", { capped: true, size: 1048576 });
BSON ve Veri Tipleri
BSON, JSON'un ikili (binary) ve genişletilmiş halidir. JSON'da bulunmayan tarih, ObjectId, 64 bit tam sayı, kesin ondalık sayı ve ikili veri gibi tipleri destekler; ayrıca makinelerin daha hızlı okuyabileceği bir formattır.
| Tip | Örnek | Ne zaman kullanılır? |
|---|---|---|
| String | "Ertuğrul" | Metinler (UTF-8) |
| Int32 / Int64 | NumberInt(20), NumberLong("9007199254740993") | Tam sayılar, büyük sayaçlar |
| Double | 3.14 | Varsayılan sayı tipi, ölçümler |
| Decimal128 | NumberDecimal("19.99") | Para ve kesinlik gereken ondalıklar |
| Boolean | true | Evet/hayır bayrakları |
| Date | new Date() | Zaman bilgisi (UTC, milisaniye) |
| ObjectId | ObjectId("651f…") | _id ve referanslar |
| Array | ["Node.js", "MongoDB"] | Listeler, etiketler |
| Object | { city: "İstanbul" } | Gömülü (embedded) dokümanlar |
| Null | null | Bilinçli olarak boş bırakılan değer |
| Binary | BinData(0, "…") | Küçük dosyalar, hash değerleri |
db.products.insertOne({
name: "Mekanik Klavye", // String
stock: NumberInt(42), // Int32
price: NumberDecimal("2499.90"), // Decimal128
rating: 4.7, // Double
inStock: true, // Boolean
tags: ["klavye", "rgb"], // Array
dimensions: { w: 44, h: 4, d: 13 },// Object
discount: null, // Null
createdAt: new Date() // Date
});
// Bir alanın tipine göre filtreleme
db.products.find({ price: { $type: "decimal" } });
Dikkat: Para değerlerini Double olarak saklamayın. JavaScript'te
0.1 + 0.2sonucu0.30000000000000004olur ve bu hata faturalara yansır.Decimal128kullanın ya da tutarları kuruş cinsinden tam sayı olarak saklayın (249990).
_id ve ObjectId
Her dokümanda benzersiz bir _id alanı bulunur. Siz vermezseniz sürücü otomatik olarak bir ObjectId üretir. ObjectId 12 byte'tan oluşur:
- 4 byte: Saniye cinsinden oluşturulma zamanı.
- 5 byte: Her makine ve süreç için üretilen rastgele bir değer.
- 3 byte: Rastgele bir başlangıç değerinden artan sayaç.
Bu yapı sayesinde ObjectId'ler sunucuya sormadan, farklı makinelerde çakışmadan üretilebilir ve kabaca oluşturulma sırasına göre sıralanır:
const id = ObjectId();
id.getTimestamp(); // ISODate("2026-02-18T09:41:12.000Z")
// _id'ye göre ters sıralama ≈ en son eklenenler
db.users.find().sort({ _id: -1 }).limit(5);
// _id istediğiniz tipte olabilir; benzersiz olması yeterli
db.users.insertOne({ _id: "ertucuk", name: "Ertuğrul Mert" });
_id alanı sonradan değiştirilemez (immutable). Değiştirmeniz gerekirse dokümanı yeni bir _id ile ekleyip eskisini silmeniz gerekir.
Doküman Sınırları
- Tek bir doküman en fazla 16 MB olabilir. Bu sınır genellikle sınırsız büyüyen dizilerin bir uyarı işaretidir.
- Dokümanlar en fazla 100 seviye iç içe olabilir.
- Alan adlarını
$ile başlatmaktan ve içinde nokta kullanmaktan kaçının; sorgularda operatörlerle karışır. - Görsel, video ve PDF gibi büyük dosyalar için GridFS'i ya da S3 gibi bir nesne depolama hizmetini kullanın; veritabanında yalnızca dosyanın adresini tutun.
Kurulum
MongoDB'yi kullanmaya başlamanın üç yolu vardır: bilgisayarınıza yerel olarak kurmak, Docker ile bir konteynerde çalıştırmak ya da MongoDB Atlas'ta ücretsiz bir bulut kümesi oluşturmak. Öğrenme aşamasında Atlas veya Docker en zahmetsiz seçeneklerdir.
macOS (Homebrew)
# Gerekirse Xcode komut satırı araçları
xcode-select --install
brew tap mongodb/brew
brew update
brew install mongodb-community@8.0
# Servisi başlatma ve durdurma
brew services start mongodb-community@8.0
brew services stop mongodb-community@8.0
# Kabuğa bağlanma
mongosh
Windows
- MongoDB Download Center'dan Community Server için MSI paketini indirin.
- Kurulumda Complete seçeneğini ve Install MongoDB as a Service kutusunu işaretli bırakın; MongoDB her açılışta arka planda çalışır.
- İsterseniz aynı ekranda MongoDB Compass'ı da kurun.
mongoshayrı bir paket olarak gelir; MongoDB Shell sayfasından indirip kurun.
# PowerShell: servisin çalıştığını kontrol edin
Get-Service MongoDB
# Kabuğa bağlanın
mongosh
Ubuntu / Debian
Ubuntu'nun kendi depolarındaki paket genellikle eskidir; resmi MongoDB deposunu ekleyerek kurmak gerekir. Aşağıdaki komutlar Ubuntu 24.04 (noble) içindir; 22.04 kullanıyorsanız noble yerine jammy yazın.
sudo apt-get install -y gnupg curl
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
sudo systemctl start mongod
sudo systemctl enable mongod # açılışta otomatik başlasın
sudo systemctl status mongod
Docker ile Kurulum
Docker, bilgisayarınıza hiçbir şey kurmadan saniyeler içinde temiz bir MongoDB ortamı sağlar:
docker run -d --name mongodb -p 27017:27017 -v mongo-data:/data/db -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=gizli-sifre mongo:8
# Konteynerin içindeki kabuğa bağlanma
docker exec -it mongodb mongosh -u admin -p gizli-sifre
Projelerde aynı yapılandırmayı docker-compose.yml ile tutmak daha pratiktir:
services:
mongodb:
image: mongo:8
container_name: mongodb
restart: unless-stopped
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: gizli-sifre
volumes:
- mongo-data:/data/db
volumes:
mongo-data:
Dikkat:
-vile bir volume bağlamazsanız konteyner silindiğinde tüm verileriniz de silinir.
MongoDB Atlas (Bulut)
Atlas, MongoDB'nin kendi yönettiği bulut hizmetidir. Kurulum, yedekleme ve güncellemelerle uğraşmazsınız; Vercel gibi platformlarda yayınlanan projeler için en yaygın tercihtir.
- MongoDB Atlas'ta ücretsiz bir hesap açın.
- Ücretsiz M0 kümesini (512 MB) seçin ve uygulamanıza yakın bir bölge belirleyin (örneğin Frankfurt).
- Database Access bölümünde bir veritabanı kullanıcısı ve güçlü bir şifre oluşturun.
- Network Access bölümünde bağlantıya izin verilecek IP adreslerini ekleyin.
- Connect → Drivers adımından bağlantı dizesini (connection string) kopyalayın.
Dikkat: Vercel gibi sabit IP'si olmayan platformlar için
0.0.0.0/0(her yerden erişim) izni gerekebilir. Bu durumda veritabanı kullanıcınızın şifresi mutlaka uzun ve rastgele olmalı, kullanıcıya da yalnızca ihtiyacı olan veritabanında yetki verilmelidir.
Araçlar: mongosh, Compass ve Diğerleri
- mongosh: Komut satırı kabuğu. Sorgu çalıştırmak, veri eklemek ve yönetim işleri için kullanılır; tam bir JavaScript ortamıdır.
- MongoDB Compass: Resmi grafik arayüz. Dokümanları görsel olarak incelemenizi, sorgu yazmanızı, indeks oluşturmanızı,
explainplanlarını okumanızı ve aggregation pipeline'ları adım adım kurmanızı sağlar. - VS Code için MongoDB eklentisi: Editörden bağlanıp "playground" dosyalarında sorgu çalıştırabilirsiniz.
- Atlas Data Explorer: Atlas arayüzündeki tarayıcı tabanlı veri görüntüleyici.
mongosh Temel Komutları
show dbs // veritabanlarını listele
use portfolio // veritabanına geç (yoksa ilk yazmada oluşur)
db // aktif veritabanının adı
show collections // koleksiyonları listele
db.stats() // veritabanı istatistikleri
db.users.countDocuments() // koleksiyondaki doküman sayısı
db.users.drop() // koleksiyonu sil
db.dropDatabase() // aktif veritabanını sil
help // yardım
exit // çıkış
mongosh bir JavaScript ortamı olduğu için değişken, döngü ve fonksiyon kullanabilirsiniz. Örneğin test verisi üretmek için:
for (let i = 1; i <= 100; i++) {
db.logs.insertOne({
level: i % 10 === 0 ? "error" : "info",
message: "Test kaydı " + i,
createdAt: new Date()
});
}
db.logs.countDocuments({ level: "error" }); // 10
Bağlantı Dizesi (Connection String)
Uygulamanız veritabanına bir bağlantı dizesiyle bağlanır. Atlas'tan aldığınız bir bağlantı dizesi şuna benzer:
mongodb+srv://portfolioApp:s1fre@cluster0.ab12c.mongodb.net/portfolio?retryWrites=true&w=majority&appName=portfolio
mongodb+srv://: Sunucu adreslerini DNS üzerinden bulan biçim (Atlas). Yerel sunucular içinmongodb://localhost:27017kullanılır.portfolioApp:s1fre: Kullanıcı adı ve şifre. Şifrede@,:,/gibi özel karakterler varsa URL kodlamasıyla yazılmalıdır (@→%40).cluster0.ab12c.mongodb.net: Kümenin adresi./portfolio: Varsayılan veritabanı.retryWrites=true: Geçici ağ hatalarında yazma işlemleri bir kez otomatik tekrar denenir.w=majority: Yazma işlemi, replica set üyelerinin çoğunluğuna ulaştığında onaylanır.appName: Loglarda ve izleme ekranlarında uygulamanızı tanımlar.
Bağlantı dizesini her zaman ortam değişkeninde tutun:
# .env.local
MONGODB_URI="mongodb+srv://portfolioApp:s1fre@cluster0.ab12c.mongodb.net/portfolio?retryWrites=true&w=majority"
Dikkat: Bağlantı dizesini asla koda gömmeyin ve Git'e göndermeyin.
.envdosyalarının.gitignoreiçinde olduğundan emin olun; yanlışlıkla paylaştıysanız şifreyi hemen değiştirin.
CRUD İşlemleri
Veritabanındaki temel işlemler dört başlıkta toplanır: Create (ekleme), Read (okuma), Update (güncelleme) ve Delete (silme). Bu bölümdeki örnekler mongosh içindir; Node.js sürücüsünde de metot adları aynıdır, yalnızca başlarına await eklenir.
Create: Doküman Eklemek
db.users.insertOne({
name: "Ertuğrul Mert",
email: "ertu@example.com",
age: 20,
role: "Developer",
skills: ["Node.js", "MongoDB"],
createdAt: new Date()
});
// { acknowledged: true, insertedId: ObjectId("...") }
db.users.insertMany([
{ name: "Utku Kösem", role: "Designer", age: 22 },
{ name: "Ayşe Yılmaz", role: "Developer", age: 27 },
{ name: "Mehmet Demir", role: "DevOps", age: 31 }
]);
insertMany varsayılan olarak sıralı (ordered) çalışır: bir dokümanda hata olursa (örneğin aynı e-posta için benzersiz indeks ihlali) kalanlar eklenmez. { ordered: false } verirseniz hatalı olanlar atlanır, diğerleri eklenmeye devam eder.
db.users.insertMany(docs, { ordered: false });
Read: Doküman Okumak
// Tüm dokümanlar
db.users.find();
// Koşula uyanlar
db.users.find({ role: "Developer" });
// Tek bir doküman (ilk eşleşen) veya null
db.users.findOne({ email: "ertu@example.com" });
// Sıralama, atlama ve sınırlama
db.users.find().sort({ createdAt: -1 }).skip(10).limit(10);
// Sayma
db.users.countDocuments({ role: "Developer" }); // filtreye uyan kesin sayı
db.users.estimatedDocumentCount(); // tüm koleksiyon, çok hızlı tahmini sayı
// Bir alandaki benzersiz değerler
db.users.distinct("role"); // ["Designer", "DevOps", "Developer"]
find doğrudan bir dizi değil, bir cursor döndürür. mongosh ilk 20 sonucu gösterir, devamı için it yazabilirsiniz. Node.js'te ise sonuçları toArray() ile diziye çevirir ya da for await ile tek tek dolaşırsınız.
Projeksiyon: Sadece Gereken Alanlar
Projeksiyon, sorgunun ikinci parametresidir ve hangi alanların döneceğini belirler. Gereksiz alanları getirmemek hem ağ trafiğini hem belleği azaltır:
// Yalnızca name ve email (_id varsayılan olarak gelir)
db.users.find({}, { name: 1, email: 1 });
// _id'yi de gizle
db.users.find({}, { name: 1, email: 1, _id: 0 });
// Belirli alanları hariç tut
db.users.find({}, { password: 0, __v: 0 });
// Dizinin yalnızca ilk 3 elemanı
db.posts.find({}, { title: 1, comments: { $slice: 3 } });
Dahil etme (1) ve hariç tutma (0) aynı projeksiyonda karıştırılamaz; tek istisnası _id: 0'dır.
Update: Güncellemek
// İlk eşleşen dokümanı güncelle
db.users.updateOne(
{ email: "ertu@example.com" },
{ $set: { role: "Full Stack Developer" } }
);
// { matchedCount: 1, modifiedCount: 1, ... }
// Eşleşen tüm dokümanları güncelle
db.users.updateMany(
{ role: "Developer" },
{ $set: { team: "Ürün" } }
);
// Dokümanın tamamını (_id hariç) yenisiyle değiştir
db.users.replaceOne(
{ email: "ertu@example.com" },
{ name: "Ertuğrul Mert", email: "ertu@example.com", role: "Full Stack Developer" }
);
Sonuçtaki matchedCount filtreye uyan, modifiedCount gerçekten değişen doküman sayısıdır. Değer zaten aynıysa doküman eşleşir ama değişmiş sayılmaz.
Upsert: Yoksa Ekle, Varsa Güncelle
upsert: true seçeneğiyle filtreye uyan doküman yoksa yeni bir doküman oluşturulur. $setOnInsert içindeki alanlar yalnızca ekleme sırasında yazılır:
db.pageViews.updateOne(
{ path: "/blog/mongo" },
{
$inc: { views: 1 },
$set: { lastViewedAt: new Date() },
$setOnInsert: { firstViewedAt: new Date() }
},
{ upsert: true }
);
Güncelleme Operatörleri
db.users.updateOne({ _id: id }, {
$set: { "address.city": "İstanbul" }, // alan ekle/değiştir (iç içe alan da olur)
$unset: { tempToken: "" }, // alanı tamamen kaldır
$inc: { loginCount: 1 }, // sayıyı artır (eksi değerle azalt)
$mul: { score: 1.1 }, // sayıyı çarp
$rename: { fullname: "name" }, // alanın adını değiştir
$min: { lowestScore: 40 }, // yalnızca daha küçükse yaz
$max: { highestScore: 95 }, // yalnızca daha büyükse yaz
$currentDate: { updatedAt: true } // sunucu saatini yaz
});
İpucu: Bir sayacı artırmak için dokümanı okuyup değeri uygulamada artırıp geri yazmayın; aynı anda gelen iki istek birbirinin değişikliğini ezer.
$incsunucuda atomik çalışır ve bu yarış durumunu (race condition) ortadan kaldırır.
Dizi Güncelleme Operatörleri
// Diziye eleman ekle
db.users.updateOne({ _id: id }, { $push: { skills: "Docker" } });
// Birden fazla eleman ekle ve dizide yalnızca son 10 elemanı tut
db.users.updateOne({ _id: id }, {
$push: { recentSearches: { $each: ["mongodb", "redis"], $slice: -10 } }
});
// Yalnızca dizide yoksa ekle (küme gibi davranır)
db.users.updateOne({ _id: id }, { $addToSet: { skills: "MongoDB" } });
// Koşula uyan elemanları çıkar
db.users.updateOne({ _id: id }, { $pull: { skills: "jQuery" } });
db.posts.updateOne({ _id: postId }, { $pull: { comments: { spam: true } } });
// Son elemanı (1) veya ilk elemanı (-1) çıkar
db.users.updateOne({ _id: id }, { $pop: { skills: 1 } });
Dizinin içindeki belirli bir elemanı güncellemek için konumsal (positional) operatörler kullanılır:
// $ : filtrede eşleşen İLK elemanı günceller
db.posts.updateOne(
{ _id: postId, "comments.author": "utku" },
{ $set: { "comments.$.approved": true } }
);
// $[] : dizideki TÜM elemanları günceller
db.students.updateOne({ _id: id }, { $inc: { "scores.$[]": 5 } });
// $[isim] + arrayFilters : koşula uyan elemanları günceller
db.students.updateMany(
{},
{ $set: { "grades.$[g].passed": true } },
{ arrayFilters: [{ "g.score": { $gte: 50 } }] }
);
Delete: Silmek
// İlk eşleşeni sil
db.users.deleteOne({ email: "utku@example.com" });
// Eşleşen tümünü sil
db.logs.deleteMany({ createdAt: { $lt: new Date("2026-01-01") } });
// Koleksiyondaki HER ŞEYİ sil (koleksiyon ve indeksler kalır)
db.logs.deleteMany({});
Kullanıcı verisi gibi geri dönüşü önemli kayıtlarda kalıcı silme yerine soft delete tercih edilebilir: dokümana bir deletedAt tarihi yazılır ve sorgularda bu alanı olmayanlar getirilir.
db.users.updateOne({ _id: id }, { $set: { deletedAt: new Date() } });
db.users.find({ deletedAt: { $exists: false } });
Dikkat:
deleteMany({})veupdateMany({}, ...)boş filtreyle koleksiyondaki tüm dokümanları etkiler. Filtre değişkeni yanlışlıkla boş kalırsa bütün veriniz gidebilir; silme işlemlerinden önce aynı filtreylecountDocumentsçalıştırmak iyi bir alışkanlıktır.
findOneAndUpdate ve Benzerleri
findOneAndUpdate, findOneAndReplace ve findOneAndDelete işlemi atomik olarak yapar ve dokümanın kendisini döndürür. Klasik kullanım alanı, sıra numarası üreten sayaçlardır:
// mongosh
db.counters.findOneAndUpdate(
{ _id: "orderNo" },
{ $inc: { seq: 1 } },
{ upsert: true, returnNewDocument: true }
);
// { _id: "orderNo", seq: 1043 }
// Node.js sürücüsünde aynı seçenek: { upsert: true, returnDocument: "after" }
Toplu İşlemler: bulkWrite
Çok sayıda farklı işlemi tek tek göndermek yerine tek bir istekte göndermek ağ gidiş-dönüşlerini azaltır ve ciddi hız kazandırır:
db.products.bulkWrite([
{ insertOne: { document: { name: "Mouse", price: NumberDecimal("899.90"), stock: 10 } } },
{ updateOne: { filter: { name: "Klavye" }, update: { $inc: { stock: -1 } } } },
{ updateMany: { filter: { stock: 0 }, update: { $set: { inStock: false } } } },
{ deleteMany: { filter: { discontinued: true } } }
]);
Sorgu Operatörleri
MongoDB filtreleri, $ ile başlayan operatörlerden oluşur. Birden fazla koşulu aynı nesneye yazdığınızda aralarında otomatik olarak VE (AND) ilişkisi kurulur.
Karşılaştırma Operatörleri
db.users.find({ age: { $eq: 20 } }); // eşittir (kısaca { age: 20 })
db.users.find({ role: { $ne: "Designer" } }); // eşit değildir
db.users.find({ age: { $gt: 18 } }); // büyüktür
db.users.find({ age: { $gte: 18, $lte: 30 } }); // 18 ile 30 arası (dahil)
db.users.find({ age: { $lt: 18 } }); // küçüktür
db.users.find({ role: { $in: ["Developer", "DevOps"] } }); // listedekilerden biri
db.users.find({ role: { $nin: ["Intern", "Guest"] } }); // listedekilerden hiçbiri
// Tarih aralığı: Şubat 2026'da oluşturulanlar
db.users.find({
createdAt: { $gte: new Date("2026-02-01"), $lt: new Date("2026-03-01") }
});
Mantıksal Operatörler
// Örtük VE: role Developer VE yaş 25'ten büyük
db.users.find({ role: "Developer", age: { $gt: 25 } });
// VEYA
db.users.find({ $or: [{ role: "Developer" }, { age: { $lt: 20 } }] });
// Aynı alanda iki ayrı koşul grubu gerekiyorsa açık $and kullanılır
db.users.find({
$and: [
{ $or: [{ role: "Developer" }, { role: "DevOps" }] },
{ $or: [{ city: "İstanbul" }, { remote: true }] }
]
});
// Hiçbiri değil
db.users.find({ $nor: [{ role: "Intern" }, { banned: true }] });
// Değil: yaşı 18'den büyük OLMAYANLAR (yaş alanı olmayanlar da dahil)
db.users.find({ age: { $not: { $gt: 18 } } });
Alan Varlığı ve Tip Kontrolü
db.users.find({ phone: { $exists: true } }); // phone alanı olanlar
db.users.find({ phone: { $exists: false } }); // phone alanı olmayanlar
db.users.find({ age: { $type: "string" } }); // yaşı yanlışlıkla metin olarak kaydedilmişler
// Dikkat: null sorgusu hem değeri null olanları hem alanı hiç olmayanları getirir
db.users.find({ phone: null });
Dizi Sorguları
// Dizide bu değer VARSA eşleşir
db.users.find({ skills: "MongoDB" });
// Dizinin tamamı birebir aynıysa (sıra dahil) eşleşir
db.users.find({ skills: ["Node.js", "MongoDB"] });
// Bu değerlerin HEPSİNİ içerenler (sıra önemsiz)
db.users.find({ skills: { $all: ["Node.js", "MongoDB"] } });
// Dizi uzunluğu tam olarak 3 olanlar
db.users.find({ skills: { $size: 3 } });
// Nesne dizilerinde: AYNI elemanın iki koşulu birden sağlaması
db.orders.find({
items: { $elemMatch: { product: "Klavye", qty: { $gte: 2 } } }
});
Son örnekte $elemMatch kullanılmasaydı, ürünü "Klavye" olan bir eleman ile adedi 2'den fazla olan başka bir eleman da eşleşmeyi sağlardı. Nesne dizilerinde birden fazla koşul varsa $elemMatch neredeyse her zaman doğru seçimdir.
İç İçe Alanlar ve Nokta Notasyonu
// Gömülü dokümanın bir alanına göre
db.users.find({ "address.city": "İstanbul" });
// Nesne dizisindeki bir alana göre
db.posts.find({ "comments.author": "utku" });
// Dizide belirli bir indeks
db.users.find({ "skills.0": "Node.js" }); // ilk yeteneği Node.js olanlar
Dikkat:
{ address: { city: "İstanbul" } }biçimindeki sorgu, gömülü dokümanın tamamen ve aynı alan sırasıyla eşleşmesini ister;addressiçinde başka bir alan varsa sonuç gelmez. İç içe alanları sorgularken nokta notasyonunu kullanın.
Regex ile Arama
// "ert" ile başlayan isimler (büyük/küçük harf duyarsız)
db.users.find({ name: { $regex: "^ert", $options: "i" } });
db.users.find({ name: /^ert/i }); // aynı sorgunun kısa yazımı
// İçinde "dev" geçen e-postalar
db.users.find({ email: /dev/ });
^ ile başlayan ve büyük/küçük harf duyarlı regex'ler indeksi verimli kullanabilir. /dev/ gibi baştan sabitlenmemiş ya da i bayraklı aramalar ise tüm indeksi taramak zorunda kalır. Türkçe büyük/küçük harf farkını önemsemeyen hızlı eşitlik aramaları için collation kullanmak daha doğrudur:
db.users.createIndex(
{ name: 1 },
{ collation: { locale: "tr", strength: 2 } } // strength 2: büyük/küçük harf duyarsız
);
db.users
.find({ name: "ertuğrul mert" })
.collation({ locale: "tr", strength: 2 }); // "Ertuğrul Mert" ile eşleşir
Metin Araması (Text Index)
Birden fazla alanda kelime bazlı arama için metin indeksi oluşturulur. Türkçe dil desteği sayesinde kökler ve etkisiz kelimeler (stop words) Türkçe kurallarına göre işlenir:
db.posts.createIndex(
{ title: "text", content: "text" },
{ default_language: "turkish", weights: { title: 10, content: 1 } }
);
db.posts
.find(
{ $text: { $search: "mongodb indeks" } },
{ title: 1, score: { $meta: "textScore" } }
)
.sort({ score: { $meta: "textScore" } });
// Tam ifade aramak için tırnak, kelimeyi hariç tutmak için eksi
db.posts.find({ $text: { $search: "\"veri modelleme\" -sql" } });
Bir koleksiyonda yalnızca bir metin indeksi olabilir. Yazım hatalarına toleranslı, otomatik tamamlamalı ve gelişmiş sıralamalı arama gerekiyorsa Atlas Search daha güçlü bir seçenektir.
Sıralama, Limit ve Sayfalama
sort ile sıralama yapılır: 1 artan, -1 azalan sıradır. Birden fazla alan verildiğinde sıralama soldan sağa uygulanır.
db.posts.find().sort({ pinned: -1, createdAt: -1 }); // önce sabitlenenler, sonra en yeniler
skip ve limit ile Sayfalama
const page = 3;
const pageSize = 10;
db.posts
.find({ published: true })
.sort({ createdAt: -1 })
.skip((page - 1) * pageSize)
.limit(pageSize);
Bu yöntem basittir ama skip atlanan dokümanları yine de tek tek okur. 10.000'inci sayfaya gitmek, önceki 99.990 dokümanı taramak demektir. Ayrıca sayfalar arasında yeni bir kayıt eklenirse aynı doküman iki sayfada görünebilir.
Cursor (Keyset) Tabanlı Sayfalama
Büyük veri setlerinde ve "daha fazla yükle" tarzı sonsuz listelerde, son görülen kaydın değerinden devam etmek çok daha verimlidir:
// İlk sayfa
const firstPage = db.posts.find().sort({ _id: -1 }).limit(10).toArray();
const lastId = firstPage[firstPage.length - 1]._id;
// Sonraki sayfa: son görülen _id'den küçükler
db.posts.find({ _id: { $lt: lastId } }).sort({ _id: -1 }).limit(10);
// Tarihe göre sıralıyorsanız, eşit tarihler için _id'yi eşitlik bozucu olarak ekleyin
db.posts.find({
$or: [
{ createdAt: { $lt: last.createdAt } },
{ createdAt: last.createdAt, _id: { $lt: last._id } }
]
}).sort({ createdAt: -1, _id: -1 }).limit(10);
Bu yöntem, sıralama alanında bir indeks olduğu sürece hangi sayfada olursanız olun aynı hızda çalışır.
İndeksleme
İndeks, bir kitabın sonundaki dizin gibidir. İndeks yoksa MongoDB aradığınız dokümanı bulmak için koleksiyondaki her dokümanı okur (COLLSCAN). İndeks varsa sıralı bir ağaç yapısında (B-tree) doğrudan ilgili kayıtlara gider (IXSCAN). Milyonlarca dokümanlık bir koleksiyonda bu, saniyeler ile milisaniyeler arasındaki farktır.
Tek Alanlı İndeks
db.users.createIndex({ email: 1 }); // artan sıralı indeks
db.users.createIndex({ createdAt: -1 }); // azalan sıralı indeks
db.users.getIndexes(); // mevcut indeksleri listele
db.users.dropIndex("createdAt_-1"); // isimle indeks sil
Her koleksiyonda _id alanı için otomatik oluşturulan benzersiz bir indeks bulunur ve silinemez.
Bileşik İndeks ve ESR Kuralı
Birden fazla alan içeren indekslere bileşik (compound) indeks denir. Alanların sırası çok önemlidir; indeks, soldan başlayan alan gruplarını (prefix) kullanan sorgulara hizmet eder:
db.posts.createIndex({ authorId: 1, published: 1, createdAt: -1 });
// Bu indeksi KULLANABİLEN sorgular:
db.posts.find({ authorId: id });
db.posts.find({ authorId: id, published: true });
db.posts.find({ authorId: id, published: true }).sort({ createdAt: -1 });
// KULLANAMAYAN sorgu (en soldaki authorId yok):
db.posts.find({ published: true }).sort({ createdAt: -1 });
Bileşik indekste alanları sıralarken ESR kuralı iyi bir başlangıç noktasıdır:
- Equality (eşitlik): Önce tam eşitlikle sorgulanan alanlar (
authorId,status). - Sort (sıralama): Sonra sıralama yapılan alanlar (
createdAt). - Range (aralık): En son
$gt,$lt,$ingibi aralık koşulları (price,age).
Unique, Partial ve Sparse İndeksler
// Benzersiz: aynı e-posta ikinci kez eklenemez (E11000 duplicate key hatası)
db.users.createIndex({ email: 1 }, { unique: true });
// Kısmi: yalnızca koşula uyan dokümanları indeksler, daha küçük ve hızlıdır
db.orders.createIndex(
{ createdAt: -1 },
{ partialFilterExpression: { status: "active" } }
);
// Kısmi + benzersiz: kullanıcı adı OLANLAR arasında benzersizlik
db.users.createIndex(
{ username: 1 },
{ unique: true, partialFilterExpression: { username: { $type: "string" } } }
);
// Seyrek: alanı olmayan dokümanları indekse almaz
db.users.createIndex({ phone: 1 }, { sparse: true });
TTL İndeksi: Kendiliğinden Silinen Veriler
TTL (time to live) indeksi, tarih alanına göre süresi dolan dokümanları arka planda otomatik siler. Oturumlar, doğrulama kodları ve geçici loglar için idealdir:
// createdAt'ten 24 saat sonra silinir
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 });
// Her doküman için ayrı bitiş zamanı: expiresAt tarihine gelince silinir
db.verificationCodes.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
db.verificationCodes.insertOne({
email: "ertu@example.com",
code: "482913",
expiresAt: new Date(Date.now() + 10 * 60 * 1000) // 10 dakika
});
Silme işlemi yaklaşık 60 saniyede bir çalışan bir arka plan görevi tarafından yapılır; bu yüzden bir doküman tam süresi dolduğu saniyede değil, kısa bir gecikmeyle silinir.
Coğrafi (2dsphere) İndeks
Konum verilerini GeoJSON formatında saklayıp "bana en yakın" sorguları yapabilirsiniz. Koordinatlar [boylam, enlem] sırasıyla yazılır:
db.cafes.insertOne({
name: "Moda Kahve",
location: { type: "Point", coordinates: [29.0250, 40.9850] }
});
db.cafes.createIndex({ location: "2dsphere" });
// Verilen noktaya 2 km'den yakın kafeler, yakından uzağa
db.cafes.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [29.0300, 40.9900] },
$maxDistance: 2000 // metre
}
}
});
explain() ile Sorgu Analizi
Bir sorgunun indeks kullanıp kullanmadığını ve ne kadar iş yaptığını explain ile görürsünüz:
db.users.find({ email: "ertu@example.com" }).explain("executionStats");
| Alan | Anlamı | İyi bir değer |
|---|---|---|
winningPlan.stage | Kullanılan plan. COLLSCAN tam tarama, IXSCAN indeks taraması | IXSCAN (+ FETCH) |
nReturned | Dönen doküman sayısı | — |
totalKeysExamined | Taranan indeks anahtarı sayısı | nReturned'e yakın |
totalDocsExamined | Okunan doküman sayısı | nReturned'e yakın |
executionTimeMillis | Sorgunun sunucudaki süresi | Olabildiğince düşük |
10 sonuç dönen bir sorgu 500.000 doküman okuyorsa (totalDocsExamined) eksik ya da yanlış sıralı bir indeks var demektir. Compass'taki "Explain Plan" sekmesi aynı bilgiyi görsel olarak gösterir.
İndekslerin Maliyeti
- Her indeks, her ekleme, güncelleme ve silme işleminde ayrıca güncellenir; çok fazla indeks yazmayı yavaşlatır.
- İndeksler disk alanı kaplar ve verimli çalışmaları için RAM'e sığmaları gerekir.
- Kullanılmayan indeksleri bulup kaldırın. Bir indeksi silmeden önce gizleyerek etkisini güvenle test edebilirsiniz:
// Hangi indeks kaç kez kullanılmış?
db.users.aggregate([{ $indexStats: {} }]);
// İndeksi silmeden sorgu planlayıcısından gizle, sorun çıkarsa geri aç
db.users.hideIndex("role_1");
db.users.unhideIndex("role_1");
Aggregation Framework
Aggregation pipeline, dokümanları bir dizi aşamadan (stage) geçirerek dönüştüren bir veri işleme hattıdır. Her aşama bir öncekinin çıktısını alır, işler ve bir sonrakine aktarır. SQL'deki GROUP BY, JOIN, HAVING ve hesaplanmış sütunların karşılığı burada bulunur.
orders koleksiyonu
│
▼ $match → yalnızca tamamlanmış siparişler
▼ $group → müşteriye göre toplam tutar
▼ $sort → en çok harcayandan en aza
▼ $limit → ilk 5
▼
sonuç
Temel Aşamalar
| Aşama | Görevi | SQL karşılığı |
|---|---|---|
$match | Dokümanları filtreler | WHERE / HAVING |
$project, $set | Alan seçer, yeniden adlandırır, hesaplanmış alan ekler | SELECT |
$group | Dokümanları gruplayıp toplar, sayar, ortalama alır | GROUP BY |
$sort, $limit, $skip | Sıralar ve sınırlar | ORDER BY, LIMIT, OFFSET |
$unwind | Diziyi her eleman için ayrı dokümana açar | — |
$lookup | Başka bir koleksiyonla birleştirir | LEFT JOIN |
$facet | Aynı veride birden fazla pipeline çalıştırır | Birden fazla sorgu |
$count, $sortByCount | Sayar; değere göre gruplayıp sayar | COUNT |
$out, $merge | Sonucu bir koleksiyona yazar | INSERT INTO ... SELECT |
$match ve $group
db.orders.aggregate([
{ $match: { status: "completed" } },
{
$group: {
_id: "$customerId", // gruplama anahtarı
totalSpent: { $sum: "$total" }, // toplam tutar
orderCount: { $sum: 1 }, // sipariş sayısı
averageOrder: { $avg: "$total" }, // ortalama sepet
lastOrderAt: { $max: "$createdAt" } // son sipariş tarihi
}
},
{ $match: { orderCount: { $gte: 3 } } }, // HAVING: en az 3 siparişi olanlar
{ $sort: { totalSpent: -1 } },
{ $limit: 5 }
]);
_id: null verirseniz tüm dokümanlar tek bir grupta toplanır; örneğin toplam ciroyu hesaplamak için kullanılır.
$project ve $set ile Alan Hesaplamak
db.users.aggregate([
{
$set: {
fullName: { $concat: ["$firstName", " ", "$lastName"] },
skillCount: { $size: { $ifNull: ["$skills", []] } },
joinedYear: { $year: "$createdAt" },
isSenior: { $gte: ["$experienceYears", 5] }
}
},
{ $project: { fullName: 1, skillCount: 1, joinedYear: 1, isSenior: 1, _id: 0 } }
]);
$unwind ile Dizileri Açmak
$unwind, dizideki her eleman için dokümanın bir kopyasını üretir. Etiket istatistikleri çıkarmak gibi işler için idealdir:
// En çok kullanılan 10 etiket
db.posts.aggregate([
{ $match: { published: true } },
{ $unwind: "$tags" },
{ $group: { _id: "$tags", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 }
]);
// Aynı işin kısa yazımı
db.posts.aggregate([
{ $unwind: "$tags" },
{ $sortByCount: "$tags" },
{ $limit: 10 }
]);
$lookup ile Koleksiyonları Birleştirmek
db.posts.aggregate([
{ $match: { published: true } },
{ $sort: { createdAt: -1 } },
{ $limit: 10 },
// Yazarı getir (LEFT JOIN)
{
$lookup: {
from: "users",
localField: "authorId",
foreignField: "_id",
as: "author"
}
},
{ $unwind: "$author" }, // tek elemanlı diziyi nesneye çevir
// Yorum sayısını hesapla
{
$lookup: {
from: "comments",
localField: "_id",
foreignField: "postId",
as: "comments"
}
},
{ $set: { commentCount: { $size: "$comments" } } },
{ $project: { title: 1, slug: 1, commentCount: 1, "author.name": 1 } }
]);
$lookup güçlüdür ama her istekte çalışan sorgularda pahalı olabilir. Önce $match, $sort ve $limit ile doküman sayısını azaltın; foreignField alanında mutlaka indeks olsun. Bir veriye sürekli $lookup ile ulaşıyorsanız, veri modelini gözden geçirmenin zamanı gelmiş olabilir.
$facet ile Tek Sorguda Çoklu Sonuç
Sayfalı bir listede hem o sayfanın kayıtlarını hem toplam kayıt sayısını tek sorguda almak için:
db.posts.aggregate([
{ $match: { published: true } },
{ $sort: { createdAt: -1 } },
{
$facet: {
items: [{ $skip: 0 }, { $limit: 10 }, { $project: { title: 1, slug: 1 } }],
total: [{ $count: "count" }],
byTag: [{ $unwind: "$tags" }, { $sortByCount: "$tags" }]
}
}
]);
Tarihe Göre Gruplama
Tarihler veritabanında UTC olarak saklanır. Aylık ya da günlük istatistik çıkarırken saat dilimini belirtmeyi unutmayın; aksi halde gece yarısına yakın kayıtlar yanlış güne düşer:
db.posts.aggregate([
{ $match: { createdAt: { $gte: new Date("2026-01-01") } } },
{
$group: {
_id: {
$dateToString: { format: "%Y-%m", date: "$createdAt", timezone: "Europe/Istanbul" }
},
posts: { $sum: 1 }
}
},
{ $sort: { _id: 1 } }
]);
// [{ _id: "2026-01", posts: 4 }, { _id: "2026-02", posts: 7 }]
// Sonucu raporlama için başka bir koleksiyona yaz
// { $merge: { into: "monthlyStats", whenMatched: "replace" } }
Aggregation Performans İpuçları
$matchve$sortaşamalarını olabildiğince başa koyun; yalnızca pipeline'ın başındaki bu aşamalar indeks kullanabilir.$projectile gereksiz alanları erken atın; sonraki aşamalara daha az veri taşınır.$lookupve$unwindöncesinde doküman sayısını$limitile azaltın.- Her aşamanın 100 MB bellek sınırı vardır. MongoDB 6.0'dan itibaren bu sınır aşıldığında geçici olarak diske yazma varsayılan olarak açıktır, ancak diske taşan sorgular belirgin şekilde yavaşlar.
- Sık çalışan ağır raporları her istekte hesaplamak yerine
$mergeile önceden hesaplanmış bir koleksiyona yazın.
Veri Modelleme
MongoDB'de şema tasarımının temel sorusu şudur: Bu veri nasıl okunacak? İlişkisel dünyadaki "veriyi normalleştir, tekrar etme" yaklaşımının yerini "birlikte okunan veriyi birlikte sakla" ilkesi alır. İlişkili verileri modellemenin iki temel yolu vardır.
Embedding (Gömme)
İlişkili veri aynı dokümanın içine yerleştirilir:
{
_id: ObjectId("..."),
name: "Ertuğrul Mert",
address: { city: "İstanbul", country: "Türkiye" },
socials: [
{ platform: "github", username: "ertucuk" },
{ platform: "instagram", username: "ertucuk" }
]
}
- Artısı: Tek sorguda tüm veri gelir; tek doküman üzerindeki güncellemeler atomiktir.
- Eksisi: Doküman büyür (16 MB sınırı); gömülü veriye bağımsız olarak erişmek zorlaşır; aynı veri birden fazla yerde tekrar ediyorsa güncellemek zahmetlidir.
Referencing (Referanslama)
Veriler ayrı koleksiyonlarda tutulur ve _id ile birbirine bağlanır:
// posts
{ _id: ObjectId("p1"), title: "MongoDB'ye Başlangıç", authorId: ObjectId("u1") }
// comments: her yorum ait olduğu yazıyı gösterir
{ _id: ObjectId("c1"), postId: ObjectId("p1"), author: "utku", text: "Harika yazı!" }
{ _id: ObjectId("c2"), postId: ObjectId("p1"), author: "ayse", text: "Teşekkürler." }
- Artısı: Dokümanlar küçük kalır; veriye bağımsız erişilir; tekrar yoktur.
- Eksisi: İlişkili veriyi almak için ikinci bir sorgu veya
$lookupgerekir.
İlişki Türüne Göre Seçim
| İlişki | Örnek | Genellikle |
|---|---|---|
| Bire-bir | Kullanıcı – profil ayarları | Gömme |
| Bire-az | Kullanıcı – birkaç adres | Gömme (dizi) |
| Bire-çok | Blog yazısı – yüzlerce yorum | Referans (yorumda postId) |
| Bire-sınırsız | Sensör – milyonlarca ölçüm | Referans veya Bucket deseni |
| Çoka-çok | Öğrenciler – dersler | İki tarafta ID dizisi veya ara koleksiyon |
Modellemenin Altın Kuralları
- Aksi için bir sebep yoksa gömün. Varsayılan tercih embedding olmalıdır.
- Veriye tek başına erişmeniz gerekiyorsa ayırın. Yorumları ayrı sayfalarda listeleyecek, tek tek düzenleyecekseniz ayrı koleksiyon mantıklıdır.
- Sınırsız büyüyen dizilerden kaçının. Bir dizi zamanla yüzlerce, binlerce elemana ulaşabiliyorsa referansa geçin.
- Şemayı sorgulara göre tasarlayın. En sık çalışan sorguların tek bir okuma ile cevaplanabildiği model genellikle doğru modeldir.
- Okuma ağırlıklıysa tekrar etmekten korkmayın. Yazar adını yazının içinde de tutmak okumayı hızlandırır; karşılığında yazar adını değiştirdiğinizde birden fazla dokümanı güncellemeniz gerekir.
Yaygın Şema Desenleri
- Subset (alt küme): Ürün dokümanında yalnızca son 10 yorumu gömün, tamamını ayrı koleksiyonda tutun.
- Computed (hesaplanmış): Yorum sayısı, ortalama puan gibi değerleri her okumada hesaplamak yerine yazma anında güncelleyin.
- Extended Reference (genişletilmiş referans): Yazarın yalnızca
_id'sini değil, sık gösterilen adını ve avatarını da saklayın. - Bucket (kova): Sensör ölçümlerini her ölçüm için ayrı doküman yerine saatlik paketler halinde saklayın.
- Schema Versioning: Dokümanlara
schemaVersionalanı ekleyip eski ve yeni şemayı uygulamada aynı anda destekleyin.
// Computed + Extended Reference + Subset bir arada
{
_id: ObjectId("..."),
title: "MongoDB'ye Başlangıç",
author: { _id: ObjectId("u1"), name: "Ertuğrul Mert", avatar: "/ertu.png" },
stats: { views: 1240, likes: 87, commentCount: 23 },
latestComments: [
{ author: "utku", text: "Harika yazı!", createdAt: ISODate("2026-02-19T10:00:00Z") }
]
}
// Yeni yorum: hem comments koleksiyonuna ekle hem yazının özetini güncelle
db.comments.insertOne({ postId, author: "ayse", text: "Teşekkürler", createdAt: new Date() });
db.posts.updateOne({ _id: postId }, {
$inc: { "stats.commentCount": 1 },
$push: {
latestComments: {
$each: [{ author: "ayse", text: "Teşekkürler", createdAt: new Date() }],
$position: 0,
$slice: 10
}
}
});
Şema Doğrulama
"Esnek şema", "kural yok" anlamına gelmez. MongoDB, $jsonSchema ile koleksiyon seviyesinde doğrulama kuralları tanımlamanızı sağlar; kurallara uymayan ekleme ve güncellemeler veritabanı tarafından reddedilir:
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email", "createdAt"],
properties: {
name: { bsonType: "string", minLength: 2, maxLength: 80 },
email: { bsonType: "string", pattern: "^.+@.+$" },
age: { bsonType: "int", minimum: 0, maximum: 120 },
role: { enum: ["Developer", "Designer", "DevOps", "Admin"] },
skills: { bsonType: "array", items: { bsonType: "string" } },
createdAt: { bsonType: "date" }
}
}
},
validationLevel: "strict", // tüm ekleme ve güncellemeleri denetle
validationAction: "error" // kurala uymayanı reddet ("warn": yalnızca logla)
});
db.users.insertOne({ name: "E", email: "gecersiz" });
// MongoServerError: Document failed validation
Var olan bir koleksiyona sonradan kural eklemek için collMod kullanılır. validationLevel: "moderate" seçeneği, zaten kurala uymayan eski dokümanları güncellerken denetimi atlar; böylece kuralları canlı bir sisteme kademeli olarak getirebilirsiniz:
db.runCommand({
collMod: "users",
validator: { $jsonSchema: { bsonType: "object", required: ["email"] } },
validationLevel: "moderate"
});
Node.js ile MongoDB
Resmi Sürücü (MongoDB Node.js Driver)
npm install mongodb
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.MONGODB_URI);
async function main() {
await client.connect();
const users = client.db("portfolio").collection("users");
const { insertedId } = await users.insertOne({
name: "Ertuğrul Mert",
role: "Full Stack Developer",
createdAt: new Date()
});
const developers = await users
.find({ role: /Developer/ }, { projection: { name: 1, role: 1 } })
.sort({ createdAt: -1 })
.limit(10)
.toArray();
await users.updateOne({ _id: insertedId }, { $set: { verified: true } });
console.log(developers);
}
main().finally(() => client.close());
MongoClient kendi içinde bir bağlantı havuzu (connection pool, varsayılan en fazla 100 bağlantı) yönetir. Bu yüzden uygulama boyunca tek bir istemci oluşturup her yerde onu kullanmalısınız; her istekte yeni bir MongoClient açmak en yaygın performans hatalarından biridir.
ObjectId ile Çalışmak
URL'den gelen ID'ler metindir. Veritabanındaki _id ise ObjectId tipindedir ve ikisi birbirine eşit sayılmaz:
import { ObjectId } from "mongodb";
const id = "651f3c2e8f1b2c3a4d5e6f7a"; // örneğin params.id
await users.findOne({ _id: id }); // ❌ null döner: string ≠ ObjectId
if (!ObjectId.isValid(id)) {
throw new Error("Geçersiz ID"); // geçersiz ID'yi baştan reddedin
}
await users.findOne({ _id: new ObjectId(id) }); // ✅
// JSON'a çevirirken ObjectId'yi metne dönüştürün
const user = await users.findOne({ _id: new ObjectId(id) });
const dto = { ...user, _id: user._id.toString() };
Next.js'te Bağlantıyı Yeniden Kullanmak
Next.js geliştirme modunda dosyalar her değişiklikte yeniden yüklenir; her seferinde yeni bir istemci oluşturursanız bağlantı sayısı hızla tükenir. Çözüm, istemciyi globalThis üzerinde saklamaktır:
// src/lib/mongodb.ts
import { MongoClient } from "mongodb";
const uri = process.env.MONGODB_URI;
if (!uri) throw new Error("MONGODB_URI tanımlı değil");
const globalForMongo = globalThis as unknown as { mongoClient?: MongoClient };
export const client = globalForMongo.mongoClient ?? new MongoClient(uri, { appName: "portfolio" });
if (process.env.NODE_ENV !== "production") {
globalForMongo.mongoClient = client; // hot reload'da aynı istemci kullanılsın
}
export const db = client.db("portfolio");
Sürücü ilk sorguda otomatik olarak bağlandığı için ayrıca connect() çağırmanız gerekmez.
Route Handler ile API
// src/app/api/projects/route.ts
import { NextResponse } from "next/server";
import type { ObjectId } from "mongodb";
import { db } from "@/lib/mongodb";
type Project = {
_id?: ObjectId;
title: string;
slug: string;
tags: string[];
createdAt: Date;
};
const projects = db.collection<Project>("projects");
export async function GET() {
const items = await projects
.find({}, { projection: { title: 1, slug: 1, tags: 1 } })
.sort({ createdAt: -1 })
.limit(20)
.toArray();
return NextResponse.json(items);
}
export async function POST(request: Request) {
const body = await request.json();
// Girdiyi doğrula: türleri kontrol etmeden veritabanına yazmayın
if (typeof body.title !== "string" || typeof body.slug !== "string") {
return NextResponse.json({ error: "title ve slug zorunlu" }, { status: 400 });
}
const result = await projects.insertOne({
title: body.title.trim(),
slug: body.slug.trim().toLowerCase(),
tags: Array.isArray(body.tags) ? body.tags.filter((tag: unknown) => typeof tag === "string") : [],
createdAt: new Date()
});
return NextResponse.json({ id: result.insertedId }, { status: 201 });
}
Server Component'te Veri Okumak
App Router'da sunucu bileşenleri doğrudan veritabanına erişebilir; API katmanına gerek kalmaz ve bağlantı bilgileri asla tarayıcıya gitmez:
// src/app/projects/page.tsx
import { db } from "@/lib/mongodb";
export default async function ProjectsPage() {
const projects = await db
.collection("projects")
.find({}, { projection: { title: 1, slug: 1 } })
.sort({ createdAt: -1 })
.toArray();
return (
<ul>
{projects.map((project) => (
<li key={project._id.toString()}>{project.title}</li>
))}
</ul>
);
}
Mongoose ile Çalışmak
Mongoose, MongoDB için en popüler ODM'dir (Object Document Mapper). Koleksiyonlara şema, doğrulama, varsayılan değerler, ilişki çözümleme (populate) ve middleware gibi özellikler ekler. Resmi sürücünün üzerine kurulu bir katmandır.
Kurulum ve Bağlantı
npm install mongoose
// src/lib/mongoose.js
import mongoose from "mongoose";
const cached = globalThis.mongoose ?? (globalThis.mongoose = { conn: null, promise: null });
export async function connectDB() {
if (cached.conn) return cached.conn;
cached.promise ??= mongoose.connect(process.env.MONGODB_URI, { bufferCommands: false });
cached.conn = await cached.promise;
return cached.conn;
}
Şema ve Model
// src/models/User.js
import mongoose from "mongoose";
const userSchema = new mongoose.Schema(
{
name: { type: String, required: [true, "İsim zorunlu"], trim: true, minlength: 2 },
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
password: { type: String, required: true, minlength: 8, select: false },
role: { type: String, enum: ["Developer", "Designer", "Admin"], default: "Developer" },
age: { type: Number, min: 0, max: 120 },
skills: [{ type: String }],
address: {
city: String,
country: { type: String, default: "Türkiye" }
}
},
{ timestamps: true } // createdAt ve updatedAt otomatik yönetilir
);
// Next.js hot reload'da modelin iki kez tanımlanmasını önler
export const User = mongoose.models.User ?? mongoose.model("User", userSchema);
unique: true bir doğrulayıcı değil, benzersiz indeks oluşturma talimatıdır. Aynı e-postayla ikinci kayıt denendiğinde hata, Mongoose'un doğrulama aşamasında değil MongoDB'nin E11000 duplicate key hatası olarak gelir.
Mongoose ile CRUD
await connectDB();
// Ekleme (şemadaki doğrulamalar çalışır)
const user = await User.create({ name: "Ertuğrul Mert", email: "ERTU@example.com", password: "..." });
user.email; // "ertu@example.com" (lowercase uygulandı)
// Okuma
const developers = await User.find({ role: "Developer" }).sort({ createdAt: -1 }).limit(10);
const one = await User.findById(id);
// Güncelleme: new → güncel dokümanı döndür, runValidators → güncellemede de doğrula
const updated = await User.findByIdAndUpdate(
id,
{ $set: { role: "Admin" } },
{ new: true, runValidators: true }
);
// Silme
await User.deleteOne({ _id: id });
// Yalnızca okuma yapacaksanız lean() düz JavaScript nesnesi döndürür, çok daha hızlıdır
const list = await User.find().select("name role").lean();
populate ile İlişkili Veriler
const postSchema = new mongoose.Schema(
{
title: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
tags: [String]
},
{ timestamps: true }
);
export const Post = mongoose.models.Post ?? mongoose.model("Post", postSchema);
// author alanındaki ObjectId'yi kullanıcı dokümanıyla değiştirir
const posts = await Post.find()
.populate("author", "name avatar") // yalnızca name ve avatar
.sort({ createdAt: -1 })
.lean();
populate, arka planda ikinci bir sorgu çalıştırır. Kullanışlıdır ama yüzlerce dokümanı ayrı ayrı populate etmek yerine, sık gösterilen alanları yukarıdaki "genişletilmiş referans" deseniyle dokümana kopyalamak çoğu zaman daha verimlidir.
Middleware, Metotlar ve Virtual Alanlar
Middleware, metot ve virtual tanımları şemaya eklenir ve mongoose.model() çağrısından önce yapılmalıdır; model oluşturulduktan sonra eklenen tanımlar o modele yansımaz.
import bcrypt from "bcryptjs";
// Kaydetmeden önce şifreyi hash'le
userSchema.pre("save", async function () {
if (!this.isModified("password")) return;
this.password = await bcrypt.hash(this.password, 12);
});
// Doküman metodu
userSchema.methods.comparePassword = function (candidate) {
return bcrypt.compare(candidate, this.password);
};
// Veritabanında saklanmayan, hesaplanan alan
userSchema.virtual("profileUrl").get(function () {
return "/u/" + this._id;
});
// Kullanım: password şemada select: false olduğu için açıkça istenir
const user = await User.findOne({ email }).select("+password");
const ok = user && (await user.comparePassword(passwordFromForm));
Mongoose mu, Resmi Sürücü mü?
- Mongoose: Şema, doğrulama, middleware ve populate hazır gelir. Birden fazla geliştiricinin çalıştığı, veri bütünlüğünün uygulama katmanında yönetildiği projelerde düzen sağlar.
- Resmi sürücü: Daha hafif ve hızlıdır, MongoDB'nin yeni özelliklerine ilk o kavuşur. TypeScript tipleri ve veritabanı tarafındaki
$jsonSchemadoğrulamasıyla birlikte çoğu proje için yeterlidir.
Öğrenme aşamasında önce resmi sürücüyle MongoDB'nin kendisini öğrenmek, ardından Mongoose'a geçmek iki aracı da daha iyi anlamanızı sağlar.
Transaction'lar
MongoDB'de tek bir doküman üzerindeki işlemler her zaman atomiktir; gömülü verilerle tasarlanmış bir şemada transaction'a çoğu zaman ihtiyaç duyulmaz. Birden fazla dokümanın birlikte değişmesi, ya hepsinin ya hiçbirinin uygulanması gerektiğinde ise 4.0 sürümünden beri ACID transaction'lar kullanılabilir.
Klasik örnek bir para transferidir: Ali'nin hesabından düşülen tutar, Ayşe'nin hesabına eklenmeden işlem yarıda kalmamalıdır.
const session = client.startSession();
try {
await session.withTransaction(async () => {
const accounts = client.db("bank").collection("accounts");
// Bakiyesi yetiyorsa düş; yetmiyorsa null döner
const from = await accounts.findOneAndUpdate(
{ _id: "ali", balance: { $gte: 100 } },
{ $inc: { balance: -100 } },
{ session, returnDocument: "after" }
);
if (!from) throw new Error("Yetersiz bakiye"); // hata → transaction geri alınır
await accounts.updateOne({ _id: "ayse" }, { $inc: { balance: 100 } }, { session });
});
} finally {
await session.endSession();
}
- Transaction içindeki her işleme
sessionparametresini vermeyi unutmayın; verilmeyen işlem transaction'ın dışında çalışır. withTransaction, geçici hatalarda işlemi otomatik olarak yeniden dener.- Transaction'lar varsayılan olarak 60 saniyeyle sınırlıdır; kısa tutun ve içinde dış servis çağrısı yapmayın.
- Transaction'lar yalnızca replica set veya sharded cluster üzerinde çalışır. Atlas kümeleri her zaman replica set'tir.
Yerel geliştirmede transaction denemek için tek üyeli bir replica set yeterlidir:
docker run -d --name mongo-rs -p 27017:27017 mongo:8 --replSet rs0
docker exec -it mongo-rs mongosh --eval "rs.initiate()"
# Uygulamadan bağlanırken:
# mongodb://localhost:27017/?directConnection=true
Replikasyon ve Sharding
Replica Set
Replica set, aynı verinin kopyasını tutan bir grup MongoDB sunucusudur. Yazma işlemlerini kabul eden bir primary ve onun değişikliklerini oplog üzerinden takip eden secondary üyelerden oluşur. Primary çökerse kalan üyeler birkaç saniye içinde aralarından yeni bir primary seçer (election) ve uygulama kesintisiz çalışmaya devam eder. Seçimin sağlıklı yapılabilmesi için en az 3 üye önerilir.
rs.status(); // üyelerin durumu: PRIMARY, SECONDARY
rs.conf(); // replica set yapılandırması
// Bağlantı dizesinde tüm üyeler ve replica set adı verilir
// mongodb://db1:27017,db2:27017,db3:27017/portfolio?replicaSet=rs0
Read Preference
Okumaların hangi üyeden yapılacağını belirler:
primary(varsayılan): Her zaman en güncel veri.primaryPreferred: Primary yoksa secondary'den oku.secondary/secondaryPreferred: Ağır raporları primary'yi yormadan secondary'de çalıştırmak için. Veri birkaç milisaniye geriden gelebilir.nearest: Ağ gecikmesi en düşük üye; çok bölgeli kurulumlar için.
Write Concern ve Read Concern
Write concern, bir yazma işleminin ne zaman "başarılı" sayılacağını belirler. MongoDB 5.0'dan beri varsayılan değer majority'dir: veri, üyelerin çoğunluğuna ulaşmadan onay verilmez ve bir sunucu çökse bile kaybolmaz.
db.orders.insertOne(order, { writeConcern: { w: "majority", j: true } }); // diske yazıldı garantisi
db.logs.insertOne(log, { writeConcern: { w: 1 } }); // yalnızca primary onayı: daha hızlı, daha az güvenli
Read concern ise okunan verinin ne kadar "kalıcı" olacağını belirler: local (varsayılan, en güncel ama geri alınabilir), majority (çoğunluğa yazılmış, geri alınamaz veri), snapshot (transaction'larda tutarlı anlık görüntü).
Sharding
Sharding, veriyi shard adı verilen birden fazla replica set'e bölerek yatay ölçekleme yapar. Uygulama doğrudan shard'lara değil, sorguları doğru shard'a yönlendiren mongos yönlendiricisine bağlanır; hangi verinin nerede olduğu bilgisi config server'larda tutulur.
sh.enableSharding("shop");
// Hashed shard key: veriyi shard'lar arasında eşit dağıtır
sh.shardCollection("shop.orders", { customerId: "hashed" });
// Ranged shard key: aralık sorgularını tek shard'da tutar
sh.shardCollection("shop.events", { tenantId: 1, createdAt: 1 });
Sharding'in başarısı shard key seçimine bağlıdır: Çok sayıda farklı değere sahip olmalı (yüksek kardinalite), yazmaları shard'lara eşit dağıtmalı ve en sık sorgular shard key'i içermelidir. Sürekli artan bir alan (örneğin yalnızca createdAt) tüm yeni yazmaları tek bir shard'a yığar.
İpucu: Çoğu proje sharding'e hiç ihtiyaç duymaz. Doğru indeksler, iyi bir şema ve bir replica set, milyonlarca dokümana kadar rahatlıkla yeter. Sharding'i gerçek bir ölçek sorunu yaşadığınızda düşünün.
Change Streams ile Gerçek Zamanlı Veri
Change stream'ler, bir koleksiyondaki değişiklikleri (ekleme, güncelleme, silme) anlık olarak dinlemenizi sağlar. Bildirim sistemleri, canlı panolar ve veri senkronizasyonu için idealdir; replica set gerektirir.
const orders = client.db("shop").collection("orders");
const stream = orders.watch([
{ $match: { operationType: "insert", "fullDocument.total": { $gte: 1000 } } }
]);
stream.on("change", (change) => {
console.log("Büyük sipariş geldi:", change.fullDocument._id);
// e-posta gönder, panoyu güncelle, WebSocket ile yayınla...
});
// Kaldığınız yerden devam etmek için resume token'ı saklayın
// orders.watch(pipeline, { resumeAfter: savedToken })
Güvenlik
Kimlik Doğrulama ve Yetkilendirme
Yerel bir MongoDB sunucusu varsayılan olarak kimlik doğrulama istemez. Sunucuyu yalnızca kendi bilgisayarınızda kullanmıyorsanız mutlaka kullanıcı oluşturun ve yetkilendirmeyi açın. Her uygulamaya yalnızca ihtiyaç duyduğu veritabanında, ihtiyaç duyduğu kadar yetki verin (en az yetki ilkesi):
use admin
db.createUser({
user: "portfolioApp",
pwd: passwordPrompt(), // şifreyi ekrana yazmadan sorar
roles: [{ role: "readWrite", db: "portfolio" }] // yalnızca portfolio'da okuma/yazma
});
// Raporlama için yalnızca okuma yetkisi
db.createUser({ user: "analytics", pwd: passwordPrompt(), roles: [{ role: "read", db: "portfolio" }] });
# /etc/mongod.conf
net:
port: 27017
bindIp: 127.0.0.1 # yalnızca bu makineden bağlantı kabul et
security:
authorization: enabled
NoSQL Injection
SQL injection'ın MongoDB'deki karşılığıdır. Kullanıcıdan gelen veriyi doğrudan filtreye koyarsanız, saldırgan metin yerine bir operatör nesnesi gönderebilir:
// ❌ Savunmasız giriş kontrolü (Express)
app.post("/login", async (req, res) => {
const user = await users.findOne({
email: req.body.email,
password: req.body.password
});
if (user) return res.json({ ok: true });
res.status(401).end();
});
Saldırgan şu gövdeyi gönderdiğinde password filtresi "boş olmayan herhangi bir şifre" anlamına gelir ve şifre bilinmeden giriş yapılır:
{ "email": "admin@site.com", "password": { "$ne": null } }
Korunma yolları:
- Türleri doğrulayın: E-posta ve şifrenin metin olduğunu kontrol edin; Zod gibi bir doğrulama kütüphanesi kullanın.
- Şifreyi asla sorguda karşılaştırmayın: Kullanıcıyı e-postayla bulun, şifreyi bcrypt ile uygulamada doğrulayın. Şifreleri her zaman hash'lenmiş saklayın.
- Mongoose kullanıyorsanız
mongoose.set("sanitizeFilter", true)ile filtrelerdeki$ile başlayan anahtarları etkisiz hale getirin. $whereve$functiongibi sunucuda JavaScript çalıştıran operatörlere kullanıcı girdisi vermeyin.
// ✅ Güvenli versiyon
app.post("/login", async (req, res) => {
const { email, password } = req.body;
if (typeof email !== "string" || typeof password !== "string") {
return res.status(400).end();
}
const user = await users.findOne({ email: email.toLowerCase() });
const ok = user && (await bcrypt.compare(password, user.passwordHash));
if (!ok) return res.status(401).end();
res.json({ ok: true });
});
Ağ ve Gizli Bilgiler
- Sunucuyu internete açmanız gerekmiyorsa
bindIpdeğerini127.0.0.1olarak bırakın; 27017 portunu güvenlik duvarında kapatın. - Sunucular arası bağlantılarda TLS kullanın. Atlas bağlantıları varsayılan olarak TLS ile şifrelidir.
- Atlas'ta IP erişim listesini olabildiğince dar tutun ve kullanılmayan veritabanı kullanıcılarını silin.
- Bağlantı dizelerini ortam değişkenlerinde tutun, düzenli aralıklarla şifreleri yenileyin.
- Hata mesajlarında veritabanı hatalarının ayrıntılarını kullanıcıya göstermeyin.
Yedekleme ve Veri Taşıma
MongoDB Database Tools paketindeki komutlarla veritabanınızın yedeğini alabilir, geri yükleyebilir ve verileri JSON/CSV olarak dışa aktarabilirsiniz:
# Tüm veritabanını BSON olarak klasöre yedekle
mongodump --uri="mongodb://localhost:27017/portfolio" --out=./yedek
# Sıkıştırılmış tek dosya olarak yedekle
mongodump --uri="$MONGODB_URI" --gzip --archive=portfolio-2026-02-18.gz
# Geri yükle (--drop: önce mevcut koleksiyonları siler)
mongorestore --uri="mongodb://localhost:27017" --gzip --archive=portfolio-2026-02-18.gz --drop
# Tek bir koleksiyonu JSON veya CSV olarak dışa aktar
mongoexport --uri="mongodb://localhost:27017/portfolio" --collection=users --out=users.json
mongoexport --uri="mongodb://localhost:27017/portfolio" --collection=users --type=csv --fields=name,email --out=users.csv
# JSON dosyasını içe aktar (dosya bir diziyse --jsonArray ekleyin)
mongoimport --uri="mongodb://localhost:27017/portfolio" --collection=users --file=users.json
Atlas'ın ücretli kümelerinde otomatik bulut yedekleme ve belirli bir ana geri dönme (point-in-time restore) özellikleri bulunur. Ücretsiz M0 kümesinde otomatik yedekleme yoktur; önemli verileriniz için düzenli olarak mongodump ile kendiniz yedek alın ve yedekten geri yüklemeyi en az bir kez deneyin. Denenmemiş yedek, yedek sayılmaz.
Diğer Faydalı Özellikler
Zaman Serisi Koleksiyonları
Sensör verileri, metrikler ve fiyat geçmişi gibi zamana bağlı ölçümler için optimize edilmiş koleksiyonlardır. Verileri arka planda verimli şekilde paketleyip sıkıştırır:
db.createCollection("weather", {
timeseries: { timeField: "timestamp", metaField: "sensor", granularity: "minutes" },
expireAfterSeconds: 60 * 60 * 24 * 30 // 30 günden eski ölçümleri otomatik sil
});
db.weather.insertOne({
sensor: { id: "afyon-01", type: "sicaklik" },
timestamp: new Date(),
temperature: 12.4
});
GridFS: Büyük Dosyalar
16 MB'tan büyük dosyaları parçalara bölerek MongoDB'de saklamanın standart yoludur. Yine de çoğu web uygulaması için dosyaları S3 benzeri bir nesne depolamada tutup veritabanında yalnızca adresini saklamak daha ucuz ve hızlıdır.
import fs from "node:fs";
import { GridFSBucket } from "mongodb";
const bucket = new GridFSBucket(db, { bucketName: "uploads" });
fs.createReadStream("./cv.pdf").pipe(
bucket.openUploadStream("cv.pdf", { metadata: { owner: "ertucuk" } })
);
Atlas Search ve Vector Search
Atlas üzerinde çalışan Atlas Search, Lucene tabanlı tam metin arama motorudur: yazım hatası toleransı, otomatik tamamlama ve alaka sıralaması sunar. Atlas Vector Search ise yapay zekâ modellerinin ürettiği vektörler (embedding) üzerinde anlamsal arama yapmanızı sağlar; RAG ve öneri sistemlerinde kullanılır. İkisi de aggregation pipeline'ın birer aşaması olarak çalışır:
db.posts.aggregate([
{
$search: {
index: "default",
text: { query: "mongo indks", path: ["title", "content"], fuzzy: { maxEdits: 1 } }
}
},
{ $limit: 5 },
{ $project: { title: 1, score: { $meta: "searchScore" } } }
]);
Performans İpuçları
- Sık çalışan her sorgu için uygun indeks oluşturun ve
explain("executionStats")ile doğrulayın. - Projeksiyon kullanın: Yalnızca ihtiyacınız olan alanları getirin.
- Mongoose'da
lean()kullanın: Salt okunur sorgularda dokümanları Mongoose nesnesine çevirme maliyetinden kurtulursunuz. - Tek bir
MongoClientkullanın: Bağlantı havuzunu paylaşın, her istekte yeni bağlantı açmayın. - Toplu işlemleri birleştirin: Döngü içinde tek tek
insertOneyerineinsertManyveyabulkWritekullanın. - Sınırsız büyüyen dizilerden kaçının; dokümanlarınızı küçük ve öngörülebilir tutun.
- Büyük
skipdeğerleri yerine keyset sayfalama kullanın. - Sayaçları
$incile atomik güncelleyin; okuyup değiştirip geri yazmayın. - Kesin sayı gerekmiyorsa
countDocuments()yerineestimatedDocumentCount()kullanın. - Çalışma kümesi RAM'e sığsın: Sık erişilen veriler ve indeksler belleğe sığmadığında performans hızla düşer.
- Uygulama ile veritabanını aynı bölgede tutun: Vercel fonksiyonunuz Frankfurt'ta, Atlas kümeniz Virginia'daysa her sorgu okyanusu iki kez geçer.
Sık Yapılan Hatalar
- Her istekte yeni bağlantı açmak: Özellikle serverless ortamlarda bağlantı sınırı hızla dolar. İstemciyi global olarak saklayın.
- String ile ObjectId'yi karşılaştırmak:
{ _id: "651f..." }sorgusu hiçbir şey döndürmez;new ObjectId(id)kullanın. - İndekssiz sorgular ya da gereğinden fazla indeks: İlki okumayı, ikincisi yazmayı yavaşlatır.
- Tarihleri metin olarak saklamak:
"18.02.2026"gibi değerler doğru sıralanmaz ve tarih operatörleriyle kullanılamaz. Her zamanDatetipini kullanın. - Para için Double kullanmak: Yuvarlama hataları oluşur; Decimal128 veya kuruş cinsinden tam sayı kullanın.
- Kullanıcı girdisini doğrulamadan filtreye koymak: NoSQL injection'a kapı açar.
- Şifreleri düz metin saklamak veya bağlantı dizesini repoya göndermek.
- Her şeyi ayrı koleksiyonlara bölmek: İlişkisel alışkanlıkla tasarlanmış bir şemada her sorgu bir
$lookupzincirine dönüşür. - Her şeyi tek dokümana gömmek: Sınırsız büyüyen diziler 16 MB sınırına dayanır ve her güncellemede büyük dokümanlar yeniden yazılır.
- Boş filtreyle
deleteMany/updateManyçalıştırmak: Tüm koleksiyon etkilenir. - Atlas'ta
0.0.0.0/0erişimini zayıf bir şifreyle birlikte açık bırakmak.
Hepsi Bir Arada: Basit Bir Blog Veritabanı
Şimdi öğrendiklerimizi birleştirip kullanıcılar, yazılar ve yorumlardan oluşan bir blog veritabanı kuralım. Aşağıdaki betiği mongosh'a yapıştırarak çalıştırabilirsiniz:
use blog
// 1) Doğrulama kuralıyla yazılar koleksiyonu
db.createCollection("posts", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["title", "slug", "author", "published", "createdAt"],
properties: {
title: { bsonType: "string", minLength: 3 },
slug: { bsonType: "string", pattern: "^[a-z0-9-]+$" },
tags: { bsonType: "array", items: { bsonType: "string" } },
published: { bsonType: "bool" },
createdAt: { bsonType: "date" }
}
}
}
});
// 2) Kullanıcılar
const { insertedIds } = db.users.insertMany([
{ name: "Ertuğrul Mert", username: "ertucuk", avatar: "/ertu.png", createdAt: new Date() },
{ name: "Utku Kösem", username: "utku", avatar: "/utku.png", createdAt: new Date() }
]);
const ertu = insertedIds[0];
// 3) Yazılar: yazarın sık gösterilen bilgileri gömülü (extended reference)
db.posts.insertMany([
{
title: "MongoDB'ye Başlangıç",
slug: "mongo",
author: { _id: ertu, name: "Ertuğrul Mert" },
tags: ["mongodb", "nosql", "backend"],
published: true,
stats: { views: 0, commentCount: 0 },
createdAt: new Date("2026-02-18")
},
{
title: "HTML'e Giriş",
slug: "html",
author: { _id: ertu, name: "Ertuğrul Mert" },
tags: ["html", "frontend"],
published: true,
stats: { views: 0, commentCount: 0 },
createdAt: new Date("2026-02-18")
}
]);
// 4) İndeksler: sık yapılan sorgulara göre
db.users.createIndex({ username: 1 }, { unique: true });
db.posts.createIndex({ slug: 1 }, { unique: true });
db.posts.createIndex({ published: 1, createdAt: -1 });
db.posts.createIndex({ tags: 1 });
db.posts.createIndex({ title: "text" }, { default_language: "turkish" });
db.comments.createIndex({ postId: 1, createdAt: -1 });
// 5) Yorum ekle ve yazının sayacını atomik olarak artır
const post = db.posts.findOne({ slug: "mongo" });
db.comments.insertOne({ postId: post._id, author: "utku", text: "Harika yazı!", createdAt: new Date() });
db.posts.updateOne({ _id: post._id }, { $inc: { "stats.commentCount": 1 } });
// 6) Görüntülenme: her ziyarette tek atomik işlem
db.posts.updateOne({ slug: "mongo" }, { $inc: { "stats.views": 1 } });
// 7) Ana sayfa: yayındaki son 10 yazı (indeks: published + createdAt)
db.posts
.find({ published: true }, { title: 1, slug: 1, "author.name": 1, stats: 1 })
.sort({ createdAt: -1 })
.limit(10);
// 8) Etiket sayfası
db.posts.find({ published: true, tags: "mongodb" }).sort({ createdAt: -1 });
// 9) Popüler etiketler
db.posts.aggregate([
{ $match: { published: true } },
{ $unwind: "$tags" },
{ $sortByCount: "$tags" },
{ $limit: 5 }
]);
// 10) Bir yazı ve son 20 yorumu
db.posts.aggregate([
{ $match: { slug: "mongo" } },
{
$lookup: {
from: "comments",
let: { id: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$postId", "$$id"] } } },
{ $sort: { createdAt: -1 } },
{ $limit: 20 }
],
as: "comments"
}
}
]);
// 11) Sonuçları kontrol et: indeks kullanılıyor mu?
db.posts.find({ published: true }).sort({ createdAt: -1 }).explain("executionStats");
Bu küçük örnek; şema doğrulamayı, genişletilmiş referans ve hesaplanmış alan desenlerini, atomik sayaçları, sorgu kalıplarına göre tasarlanmış indeksleri ve aggregation'ı bir arada gösteriyor. Gerçek bir uygulamada aynı yapıyı Next.js route handler'ları veya Mongoose modelleriyle birkaç dosyaya bölmeniz yeterlidir.
Hızlı Başvuru Listesi
- mongosh:
show dbs,use,show collections,db.stats(),db.dropDatabase(),exit - Ekleme:
insertOne,insertMany,bulkWrite - Okuma:
find,findOne,countDocuments,estimatedDocumentCount,distinct,sort,skip,limit - Güncelleme:
updateOne,updateMany,replaceOne,findOneAndUpdate,upsert - Silme:
deleteOne,deleteMany,findOneAndDelete,drop - Karşılaştırma:
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin - Mantıksal ve eleman:
$and,$or,$nor,$not,$exists,$type - Dizi sorguları:
$all,$size,$elemMatch - Güncelleme operatörleri:
$set,$unset,$inc,$mul,$rename,$min,$max,$currentDate,$setOnInsert - Dizi güncelleme:
$push,$addToSet,$pull,$pop,$each,$slice,$,$[],arrayFilters - Aggregation:
$match,$project,$set,$group,$sort,$limit,$skip,$unwind,$lookup,$facet,$count,$sortByCount,$merge - İndeks türleri: tek alanlı, bileşik, unique, partial, sparse, TTL, text, 2dsphere
- Araçlar:
explain,$indexStats,hideIndex,mongodump,mongorestore,mongoexport,mongoimport
Sıkça Sorulan Sorular
MongoDB ücretsiz mi?
MongoDB Community Server ücretsizdir ve kendi sunucunuzda istediğiniz gibi kullanabilirsiniz (SSPL lisansı, MongoDB'yi başkalarına bir hizmet olarak satmak isteyenlere ek koşullar getirir). Bulutta ise Atlas'ın M0 kümesi 512 MB'a kadar ücretsizdir; kişisel projeler ve öğrenme için fazlasıyla yeterlidir.
MongoDB şemasız mı?
Tam olarak değil; "esnek şemalı" demek daha doğrudur. Veritabanı sizi tek bir şemaya zorlamaz, ama uygulamanızın verisi her zaman bir şemaya sahiptir. Bu şemayı Mongoose ile uygulamada, $jsonSchema ile veritabanında ya da ikisiyle birden tanımlayabilirsiniz.
MongoDB'de JOIN yok mu?
Var: $lookup aşaması bir LEFT JOIN gibi çalışır. Ancak MongoDB'nin felsefesi, birlikte okunan verileri birlikte saklayarak JOIN ihtiyacını en baştan azaltmaktır. Sürekli JOIN yapmak zorunda kalıyorsanız şemanızı gözden geçirin.
Mongoose kullanmak zorunda mıyım?
Hayır. Resmi sürücü tek başına her şeyi yapabilir ve TypeScript ile birlikte oldukça rahat bir deneyim sunar. Mongoose şema, doğrulama ve middleware gibi kolaylıklar ekler; ekip ve proje ihtiyaçlarınıza göre seçebilirsiniz.
MongoDB mi, PostgreSQL mi?
İkisi de mükemmel veritabanlarıdır. Veriniz iç içe ve değişkense, hızlı geliştirme ve JSON uyumu önemliyse MongoDB; veriniz güçlü ilişkiler ve karmaşık raporlar içeriyorsa PostgreSQL genellikle daha doğal bir seçimdir. Önemli olan aracı verinin doğasına göre seçmektir. İkisini de bilmek sizi daha iyi bir geliştirici yapar.
MongoDB öğrenmek ne kadar sürer?
CRUD işlemlerini ve temel sorguları birkaç gün içinde öğrenip küçük bir projede kullanmaya başlayabilirsiniz. İndeksleme, aggregation ve özellikle veri modellemede ustalaşmak ise gerçek projelerde, gerçek sorgu kalıplarıyla çalışırken gelir.
Sonraki Adımlar ve Kaynaklar
MongoDB temellerini öğrendikten sonra izleyebileceğiniz yol:
- Pratik: Atlas'ta ücretsiz bir küme açın ve küçük bir proje (yapılacaklar listesi, blog, not uygulaması) için şemayı kendiniz tasarlayın.
- Next.js entegrasyonu: Bu yazıdaki bağlantı ve route handler örnekleriyle projenize gerçek bir API ekleyin.
- İndeks ve explain: Projenizdeki her sorgunun
explainçıktısına bakın; COLLSCAN gördüğünüz yerlere indeks ekleyin. - Aggregation: Kendi verinizden istatistikler (en popüler içerikler, aylık kayıtlar) çıkaran pipeline'lar yazın.
- Veri modelleme: Aynı projeyi bir kez gömme, bir kez referans ağırlıklı tasarlayıp sorguları karşılaştırın.
Faydalı kaynaklar:
- MongoDB Dokümantasyonu: Tüm komut ve operatörler için resmi başvuru kaynağı.
- MongoDB University: Ücretsiz resmi kurslar ve sertifika programları.
- SQL ve MongoDB Karşılaştırması: SQL bilenler için terim ve sorgu eşleştirmeleri.
- Node.js Sürücüsü Dokümantasyonu: Resmi sürücünün tüm API'si.
- Mongoose Dokümantasyonu: Şema, model, populate ve middleware rehberleri.
- MongoDB Compass: Resmi grafik arayüz.
Sonuç
MongoDB, esnek doküman modeli, güçlü sorgu dili ve JavaScript ekosistemiyle doğal uyumu sayesinde modern uygulamalar için son derece üretken bir veritabanıdır. Bu yazıda NoSQL kavramından BSON veri tiplerine, kurulumdan CRUD işlemlerine, sorgu ve güncelleme operatörlerinden indekslere, aggregation pipeline'dan veri modellemeye, Node.js, Next.js ve Mongoose entegrasyonundan transaction, replikasyon, güvenlik ve yedeklemeye kadar MongoDB'nin neredeyse tüm temel konularını ele aldık.
Akılda tutulması gereken en önemli fikir şu: Şemayı verinin nasıl saklandığına göre değil, nasıl okunduğuna göre tasarlayın. Doğru modellenmiş dokümanlar ve sorgu kalıplarına göre oluşturulmuş indeksler, MongoDB'den alacağınız performansın büyük kısmını belirler.
Bu temel bilgilerle artık kendi projelerinizde MongoDB'yi güvenle kullanmaya başlayabilirsiniz. Bir sonraki adım, bir proje seçip şemasını kağıt üzerinde tasarlamak ve ilk koleksiyonunuzu oluşturmak olacak.