2025-11-17
DynamoDB Single-Table Design: Kapsamlı Modelleme Rehberi
DynamoDB single-table design'ı ilişkileri modelleme, GSI ve LSI seçimi, DAX optimizasyonu ve production NoSQL sistemlerinde yaygın hataları önleme konularında pratik örneklerle öğren.
Özet
Single-table design, DynamoDB için veri modelleme yaklaşımında temel bir değişimi temsil ediyor. Bu kapsamlı rehber, single-table pattern’leri ne zaman kullanacağını, one-to-one, one-to-many ve many-to-many ilişkileri nasıl modelleyeceğini, Global ve Local Secondary Index’ler arasındaki trade-off’ları, DAX caching entegrasyonunu ve pratik query optimizasyon tekniklerini ele alıyor. Çalışan TypeScript örnekleri, gerçek maliyet analizleri ve hot partition ile throttling sorunlarından kaçınmak için test edilmiş pattern’ler bulacaksın.
Single-Table Design Neden Önemli
DynamoDB ile çalışırken öğrendiğim en önemli ders, relational tablo mantığıyla düşünmenin sorun çözmekten çok sorun yarattığı oldu. Tipik yaklaşım - Users, Orders, Products için ayrı tablolar oluşturmak - birden fazla round-trip, karmaşık uygulama mantığı ve öngörülemeyen maliyetlere yol açıyor.
Single-table design, birden fazla entity type’ı generic partition ve sort key’ler kullanarak tek tabloda saklıyor. Bir customer’ı bir tablodan, order’larını başka bir tablodan çekmek yerine, her şeyi tek request’te alıyorsun. Bu sadece performansla ilgili değil; veri modellemeye yaklaşımını temelden değiştiriyor.
Temel İlkeler:
- Access pattern’ler önce: Schema’yı tasarlamadan önce her query’yi dokümante et
- Data locality: İlişkili veriyi aynı partition key kullanarak bir arada sakla
- Generic key’ler: Entity-specific isimler yerine
PKveSKkullan - Item collection’lar: İlişkili item’ları shared partition key’lerle grupla
- Attribute overloading: Aynı attribute’lar farklı entity type’ları boyunca farklı amaçlara hizmet eder
Single-Table Design Ne Zaman Kullanılmalı
Single-table design, ilişkili veriyi birlikte çekmen gerektiğinde mükemmel çalışıyor. Ne zaman iyi çalıştığı hakkında öğrendiklerim:
İyi Kullanım Senaryoları:
- Customer’ları order’larıyla birlikte çektiğin e-commerce sistemleri
- Post’ları comment ve like’larla birlikte alan sosyal platformlar
- Tenant isolation olan multi-tenant SaaS uygulamaları
- Hierarchical veri içeren content management sistemleri
- Cihaz bazında sensor reading’leri toplayan IoT platformları
Ne Zaman Kaçınmalı:
Ekiplerle çalışırken single-table design’ın her zaman doğru seçim olmadığını gördüm:
- Ekipte DynamoDB uzmanlığı yok (öğrenme eğrisi gerçek)
- Minimal ilişkili basit CRUD uygulamaları
- Ad-hoc reporting ve data warehouse senaryoları
- Tüm entity type’ları için strong consistency gerekiyor
- Farklı entity’lerin tamamen farklı access pattern’leri var
Rick Houlihan’ın 2024 güncellemesi vurguluyor: “Birlikte erişilen şeyler birlikte saklanmalı.” İlgisiz veriyi sadece bir pattern’i takip etmek için tek tabloya zorlama.
Partition Key ve Sort Key Stratejileri
Single-table design’ın temeli, key’lerini nasıl yapılandıracağını anlamakta yatıyor.
Partition Key Pattern’leri
// Entity type prefix - en yaygın pattern
PK: "CUSTOMER#123"
PK: "ORDER#456"
PK: "PRODUCT#789"
// Multi-tenant için composite partition key
PK: "TENANT#acme#USER#123"
// High-cardinality user-specific key
PK: "USER#${userId}" // Her user unique partition alır
Kaçınılması Gereken Anti-Pattern:
// Low-cardinality status key - hot partition yaratır
PK: "STATUS#active" // Tüm active user'lar BİR partition'da
// Partition başına 3,000 RCU limitine ulaştığında throttle olur
Sort Key Pattern’leri
Sort key’ler range query’leri ve hierarchical organization’ı mümkün kılıyor:
// Hierarchical sort key - prefix query'leri mümkün kılar
SK: "US#CA#SanFrancisco#94102"
// Query: begins_with(SK, "US#CA") tüm California item'larını döner
// Timestamp-based chronological ordering
SK: "2024-01-15T10:30:00#EVENT#123"
// Version control pattern
SK: "v0_item123" // Current version
SK: "v1_item123" // Previous version
SK: "v2_item123" // Older version
// İlişkiler için composite
SK: "ORDERITEM#PRODUCT#789#2024-01-15"
Best Practice’ler:
- High-cardinality partition key’ler kullan (userId, orderId, productId)
begins_withveBETWEENile range query’leri destekleyecek sort key’ler tasarla- Chronological ordering için timestamp ekle
- Entity type’ları boyunca consistent prefix’ler kullan
İlişkileri Modelleme
Her ilişki türünü çalışan örneklerle nasıl modelleyeceğini göstereyim.
One-to-One İlişkiler
İlişkili veriyi aynı item collection’da farklı sort key’lerle sakla:
interface User {
PK: string;
SK: string;
EntityType: "User";
email: string;
name: string;
}
interface UserPreferences {
PK: string;
SK: string;
EntityType: "UserPreferences";
theme: "dark" | "light";
language: string;
}
// Şöyle saklanır:
{
PK: "USER#123",
SK: "METADATA",
EntityType: "User",
email: "[email protected]",
name: "John Doe"
}
{
PK: "USER#123",
SK: "PREFERENCES",
EntityType: "UserPreferences",
theme: "dark",
language: "en"
}
// Tek query ikisini de getirir
const params = {
TableName: 'MainTable',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: {
':pk': 'USER#123'
}
};
One-to-Many İlişkiler
Item collection’lar one-to-many ilişkileri basitleştiriyor:
interface Customer {
PK: string;
SK: string;
EntityType: "Customer";
name: string;
email: string;
}
interface Order {
PK: string; // Customer PK ile aynı
SK: string; // Chronological sort key
EntityType: "Order";
orderId: string;
total: number;
status: string;
orderDate: string;
}
// Customer
{
PK: "CUSTOMER#123",
SK: "METADATA",
EntityType: "Customer",
name: "John Doe",
email: "[email protected]"
}
// Bu customer için order'lar
{
PK: "CUSTOMER#123",
SK: "ORDER#2024-01-15#456",
EntityType: "Order",
orderId: "456",
total: 99.99,
status: "delivered",
orderDate: "2024-01-15"
}
{
PK: "CUSTOMER#123",
SK: "ORDER#2024-01-20#457",
EntityType: "Order",
orderId: "457",
total: 149.99,
status: "pending",
orderDate: "2024-01-20"
}
// Customer ve TÜM order'larını tek query'de al
const getCustomerWithOrders = async (customerId: string) => {
const result = await dynamodb.query({
TableName: 'MainTable',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: {
':pk': `CUSTOMER#${customerId}`
}
});
return {
customer: result.Items?.find(item => item.SK === 'METADATA'),
orders: result.Items?.filter(item => item.SK.startsWith('ORDER#'))
};
};
// Sadece pending order'ları al
const getPendingOrders = async (customerId: string) => {
const result = await dynamodb.query({
TableName: 'MainTable',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
FilterExpression: '#status = :status',
ExpressionAttributeNames: {
'#status': 'status'
},
ExpressionAttributeValues: {
':pk': `CUSTOMER#${customerId}`,
':sk': 'ORDER#',
':status': 'pending'
}
});
return result.Items;
};
Many-to-Many İlişkiler
Many-to-many ilişkileri modellemek için adjacency list pattern kullan:
interface ProductCategory {
PK: string;
SK: string;
EntityType: "ProductCategory";
productName?: string;
categoryName?: string;
}
// Product birden fazla category'ye ait
// Forward relationship'ler
{
PK: "PRODUCT#789",
SK: "CATEGORY#Electronics",
EntityType: "ProductCategory",
categoryName: "Electronics"
}
{
PK: "PRODUCT#789",
SK: "CATEGORY#Gadgets",
EntityType: "ProductCategory",
categoryName: "Gadgets"
}
// Reverse relationship'ler (bidirectional query için ikisini de yaz)
{
PK: "CATEGORY#Electronics",
SK: "PRODUCT#789",
EntityType: "CategoryProduct",
productName: "Wireless Headphones"
}
{
PK: "CATEGORY#Gadgets",
SK: "PRODUCT#789",
EntityType: "CategoryProduct",
productName: "Wireless Headphones"
}
// Bir product için tüm category'leri query et
const getProductCategories = async (productId: string) => {
const result = await dynamodb.query({
TableName: 'MainTable',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
ExpressionAttributeValues: {
':pk': `PRODUCT#${productId}`,
':sk': 'CATEGORY#'
}
});
return result.Items;
};
// Bir category'deki tüm product'ları query et
const getCategoryProducts = async (categoryName: string) => {
const result = await dynamodb.query({
TableName: 'MainTable',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
ExpressionAttributeValues: {
':pk': `CATEGORY#${categoryName}`,
':sk': 'PRODUCT#'
}
});
return result.Items;
};
Trade-off: Her iki yönü yazmak write operation’larını ikiye katlar ama Scan operation’ı olmadan her iki yönde de efficient query’leri mümkün kılar.
Denormalization Pattern
Bazen sık erişilen veriye ekstra query olmadan ihtiyacın var:
interface Order {
PK: string;
SK: string;
EntityType: "Order";
orderId: string;
customerId: string;
// Denormalized customer data
customerName: string;
customerEmail: string;
total: number;
}
{
PK: "ORDER#456",
SK: "METADATA",
EntityType: "Order",
orderId: "456",
customerId: "123",
customerName: "John Doe", // Customer record'dan kopyalandı
customerEmail: "[email protected]", // Customer record'dan kopyalandı
total: 99.99
}
Trade-off Analizi:
- Daha hızlı read’ler: Customer detaylarını ayrıca fetch etmeye gerek yok
- Daha karmaşık write’lar: Customer name güncellemesi tüm order’larını güncellemeyi gerektirir
- Storage overhead: Customer verisi order’lar boyunca duplicate edilir
- Eventual consistency: Customer data güncellemeleri order’ları güncellemek için background job gerektirir
Denormalization’ı read sıklığı write sıklığından önemli ölçüde fazla olduğunda ve veri nadiren değiştiğinde kullan.
GSI vs LSI: Doğru Seçim
Global Secondary Index’ler ile Local Secondary Index’ler arasındaki farkları anlamak efficient access pattern’ler için kritik.
Local Secondary Index (LSI)
LSI, base table ile partition key’i paylaşır ama farklı sort key kullanır:
// Table yapısı
interface Order {
PK: string; // Partition key
SK: string; // Sort key (order tarihi)
orderId: string;
status: string;
total: number;
}
// Base table tarih bazında query yapar
{
PK: "CUSTOMER#123",
SK: "ORDER#2024-01-15#456",
orderId: "456",
status: "delivered",
total: 99.99
}
// LSI strong consistency ile status bazında query'i mümkün kılar
const lsiDefinition = {
IndexName: "LSI-Status",
KeySchema: [
{ AttributeName: "PK", KeyType: "HASH" }, // Table ile aynı
{ AttributeName: "status", KeyType: "RANGE" } // Farklı sort key
],
Projection: {
ProjectionType: "ALL"
}
};
// Customer'ın pending order'larını strong consistency ile query et
const params = {
TableName: 'Orders',
IndexName: 'LSI-Status',
KeyConditionExpression: 'PK = :pk AND #status = :status',
ExpressionAttributeNames: {
'#status': 'status'
},
ExpressionAttributeValues: {
':pk': 'CUSTOMER#123',
':status': 'pending'
},
ConsistentRead: true // Sadece LSI ile mümkün
};
LSI Özellikleri:
- Table creation’da tanımlanmalı (sonradan eklenemez)
- Base table ile partition key paylaşır
- Strongly consistent read’leri destekler
- Base table ile throughput capacity paylaşır
- Partition key value başına 10GB limit
- Table başına maksimum 5 LSI
- Ek capacity planning gerektirmez
Global Secondary Index (GSI)
GSI farklı partition ve sort key’ler kullanır, tamamen yeni access pattern’leri mümkün kılar:
// Table yapısı
interface Order {
PK: string;
SK: string;
EntityType: string;
orderId: string;
orderDate: string;
status: string;
GSI1PK: string; // Tarih bazlı query'ler için
GSI1SK: string;
}
{
PK: "CUSTOMER#123",
SK: "ORDER#456",
EntityType: "Order",
orderId: "456",
orderDate: "2024-01-15",
status: "delivered",
GSI1PK: "2024-01-15", // Tarihe göre grupla
GSI1SK: "ORDER#456"
}
// GSI Tanımı
const gsiDefinition = {
IndexName: "GSI1",
KeySchema: [
{ AttributeName: "GSI1PK", KeyType: "HASH" },
{ AttributeName: "GSI1SK", KeyType: "RANGE" }
],
Projection: {
ProjectionType: "INCLUDE",
NonKeyAttributes: ["orderId", "status", "total"]
}
};
// TÜM order'ları tarihe göre query et (tüm customer'lar için)
const getOrdersByDate = async (date: string) => {
const result = await dynamodb.query({
TableName: 'MainTable',
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :date',
ExpressionAttributeValues: {
':date': date
}
});
return result.Items;
};
GSI Özellikleri:
- Table creation’dan sonra eklenebilir veya çıkarılabilir
- Base table’dan farklı partition ve sort key’ler
- Sadece eventually consistent (strong consistency yok)
- Provisioned mode’da independent throughput
- Size limit’i yok
- Table başına maksimum 20 GSI (5’ten artırıldı)
- Cross-partition query’leri mümkün kılar
Karar Matrisi
| Gereksinim | LSI | GSI |
|---|---|---|
| Strong consistency gerekli | Yes | No |
| Farklı partition key gerekli | No | Yes |
| Table’dan sonra oluştur | No | Yes |
| Aynı partition, farklı sort order | Yes | Yes |
| Independent capacity planning | No | Yes (provisioned) |
| Cross-partition query’ler | No | Yes |
| Item size > 10GB per partition | No | Yes |
LSI Ne Zaman Kullanılmalı:
- Strong consistency gerektiğinde
- Aynı partition’ı alternative sort order ile query ederken
- Küçük dataset’ler (< 10GB per partition)
- Access pattern’ler table creation’da bilindiğinde
GSI Ne Zaman Kullanılmalı:
- Access pattern için farklı partition key gerektiğinde
- Cross-partition query’ler gerektiğinde
- Mevcut table’a yeni access pattern eklerken
- Eventually consistent read’ler kabul edilebilir olduğunda
- Büyük dataset’lerde
Sparse Index Pattern
Sadece GSI attribute’larına sahip item’lar index’e dahil edilir, storage maliyetlerini azaltır:
interface User {
PK: string;
SK: string;
EntityType: "User";
status: "active" | "inactive";
GSI1PK?: string; // Sadece active user'lar için set edilir
GSI1SK?: string;
}
// Active user - index'te
{
PK: "USER#123",
SK: "METADATA",
EntityType: "User",
status: "active",
email: "[email protected]",
GSI1PK: "ACTIVE_USERS", // GSI'a dahil
GSI1SK: "USER#123"
}
// Inactive user - index'te DEĞİL
{
PK: "USER#456",
SK: "METADATA",
EntityType: "User",
status: "inactive",
email: "[email protected]"
// GSI1PK/GSI1SK yok - index'te değil, storage tasarrufu
}
// Sadece active user'ları query et
const getActiveUsers = async () => {
const result = await dynamodb.query({
TableName: 'MainTable',
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :type',
ExpressionAttributeValues: {
':type': 'ACTIVE_USERS'
}
});
return result.Items;
};
Maliyet Tasarrufu: Sadece %10 user active ise, sparse indexing GSI storage’ı %90 azaltır.
DynamoDB Accelerator (DAX) Entegrasyonu
DAX, read-heavy workload’lar için in-memory caching ile microsecond response time’ları sağlıyor.
DAX Ne Zaman Kullanılmalı
// Senaryo 1: Read-heavy workload
// E-commerce product catalog
// - %95 read, %5 write
// - Beklenen cache hit rate: %90+
// - Fayda: 10x latency improvement + maliyet azaltma
// Senaryo 2: Hot key pattern
// Flash sale - tek product saniyede 10,000 read alıyor
// DAX olmadan: Throttling, yüksek maliyetler
// DAX ile: Read'leri DynamoDB'den offload eder
// Senaryo 3: Tekrarlı read'ler
// Aynı bölgesel veriyi tekrar tekrar query eden weather analizi
// Analiz aynı dataset üzerinde saatlerce çalışıyor
// DAX tüm dataset'i memory'de cache'liyor
DAX Ne Zaman KULLANILMAMALI
// Anti-pattern 1: Write-heavy workload
// Sık güncellemeli real-time analytics
// DAX fayda olmadan overhead ekler
// Anti-pattern 2: Strong consistency gerekli
// Anında consistency gerektiren finansal transaction'lar
// DAX sadece eventual consistency sağlar
// Anti-pattern 3: Düşük cache hit rate
// Random access pattern'lı ad-hoc query'ler
// Cache hit rate < %50 = kötü ROI
// Anti-pattern 4: Düşük trafik
// Saniyede < 100 request alan uygulama
// DAX maliyeti faydayı aşıyor
DAX Implementasyonu
import { DynamoDB } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
import AmazonDaxClient from 'amazon-dax-client';
// DAX olmadan - direkt DynamoDB
const directClient = new DynamoDB({
region: 'us-east-1'
});
const dynamodb = DynamoDBDocument.from(directClient);
// DAX ile
const daxClient = new AmazonDaxClient({
endpoints: ['my-cluster.dax.us-east-1.amazonaws.com:8111'],
region: 'us-east-1'
});
const dax = DynamoDBDocument.from(daxClient);
// Aynı API - drop-in replacement
const getProduct = async (productId: string) => {
const params = {
TableName: 'Products',
Key: {
PK: `PRODUCT#${productId}`,
SK: 'METADATA'
}
};
// İlk çağrı: DynamoDB query (5ms)
// Sonraki çağrılar: DAX cache (500μs)
const result = await dax.get(params);
return result.Item;
};
// Query operation'ları da cache'lenir
const getProductReviews = async (productId: string) => {
const params = {
TableName: 'Products',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
ExpressionAttributeValues: {
':pk': `PRODUCT#${productId}`,
':sk': 'REVIEW#'
}
};
const result = await dax.query(params);
return result.Items;
};
DAX Performance Metrikleri
const benchmarks = {
dynamodb: {
getItem: '3-5ms',
query: '5-10ms',
cost: 'Milyon read başına $0.155' // Kasım 2024 güncellemesi
},
dax: {
getItem: '200-500μs (cache hit)',
query: '300-700μs (cache hit)',
cacheMiss: '4-6ms (DynamoDB + overhead)',
cost: 'Node başına saatte $0.11 (t3.small)'
}
};
Maliyet Analizi
// Senaryo: %95 cache hit rate ile saniyede 1,000 request
// DAX olmadan:
// - Request'ler: Ayda 2.59B
// - DynamoDB maliyet: Ayda $401 (on-demand, $0.155/milyon)
// DAX ile (3-node t3.medium cluster):
// - DAX cluster: Ayda $482
// - Cache hit'ler (%95): 2.46B read (DAX tarafından servis edilir)
// - Cache miss'ler (%5): DynamoDB'den 130M read
// - DynamoDB maliyet: Ayda $20
// - Toplam: Ayda $502
// - Tasarruf: Bu ölçekte minimal; ana fayda 10x latency improvement
// Break-even noktası: %90+ cache hit rate ile ~saniyede 500 req
// Ana değer: Sadece maliyet değil, latency azaltma
Query Optimizasyon Teknikleri
Query ve Scan operation’ları arasındaki fark performans ve maliyeti dramatik şekilde etkiliyor.
Query vs Scan
// ANTI-PATTERN: Scan operation
const findUserByEmail = async (email: string) => {
const result = await dynamodb.scan({
TableName: 'Users',
FilterExpression: 'email = :email',
ExpressionAttributeValues: {
':email': email
}
});
return result.Items?.[0];
};
// - Tüm table'ı okur, application'da filtreler
// - 1M item = 1M RCU tüketir
// - Latency: 5-60 saniye
// - Maliyet: Scan başına $0.25
// BEST PRACTICE: GSI ile query
const findUserByEmailOptimized = async (email: string) => {
const result = await dynamodb.query({
TableName: 'Users',
IndexName: 'GSI-Email',
KeyConditionExpression: 'GSI1PK = :email',
ExpressionAttributeValues: {
':email': `EMAIL#${email}`
}
});
return result.Items?.[0];
};
// - Direkt partition erişimi
// - 1 item = 0.5 RCU (eventually consistent)
// - Latency: 5-10ms
// - Maliyet: Query başına $0.00000025 (1,000x daha ucuz)
Gerçek Dünya Etkisi
Milyonlarca user’lı sistemlerde çalışırken maliyet farkının dramatik olduğunu öğrendim:
// 1M user table'ında email ile user bulma
// Scan yaklaşımı:
// - Read capacity: 1,000,000 RCU
// - Süre: 45 saniye (pagination ile)
// - Query başına maliyet: $0.25
// GSI ile query:
// - Read capacity: 0.5 RCU
// - Süre: 8ms
// - Query başına maliyet: $0.00000025
// Sonuç: 1,000x maliyet azaltma, 5,000x daha hızlı
Projection Optimizasyonu
// ANTI-PATTERN: Tüm attribute'ları project et
const gsiAll = {
IndexName: 'GSI1',
Projection: {
ProjectionType: 'ALL' // Tüm item'ı duplicate eder
}
};
// Storage: 2x table size (table + full GSI copy)
// Maliyet: Yüksek
// BEST PRACTICE: Sadece gerekli attribute'ları project et
const gsiInclude = {
IndexName: 'GSI1',
Projection: {
ProjectionType: 'INCLUDE',
NonKeyAttributes: ['name', 'email', 'status']
}
};
// Storage: Minimal (key'ler + belirtilen attribute'lar)
// Maliyet: Optimize
// OPTIMAL: Full item'ı zaten fetch edeceksen sadece key'ler
const gsiKeys = {
IndexName: 'GSI1',
Projection: {
ProjectionType: 'KEYS_ONLY'
}
};
// Pattern kullan: GSI'dan ID'leri query et, sonra detaylar için BatchGetItem
Batch Operation’lar
// ANTI-PATTERN: Sequential GetItem çağrıları
const getUsersSequential = async (userIds: string[]) => {
const users = [];
for (const userId of userIds) {
const result = await dynamodb.get({
TableName: 'Users',
Key: {
PK: `USER#${userId}`,
SK: 'METADATA'
}
});
users.push(result.Item);
}
return users;
};
// 100 user = 100 round trip = 500-1000ms
// BEST PRACTICE: BatchGetItem
import { BatchGetCommand } from '@aws-sdk/lib-dynamodb';
const getUsersBatch = async (userIds: string[]) => {
const command = new BatchGetCommand({
RequestItems: {
Users: {
Keys: userIds.map(id => ({
PK: `USER#${id}`,
SK: 'METADATA'
}))
}
}
});
const result = await dynamodb.send(command);
return result.Responses?.Users || [];
};
// 100 user = 1 request (100 item'a kadar) = 50-100ms
// 5-10x daha hızlı
Filtreleme için Composite Sort Key
// ANTI-PATTERN: Query + FilterExpression
const getPendingOrdersWrong = async (customerId: string) => {
const result = await dynamodb.query({
TableName: 'Orders',
KeyConditionExpression: 'PK = :pk',
FilterExpression: '#status = :status',
ExpressionAttributeNames: {
'#status': 'status'
},
ExpressionAttributeValues: {
':pk': `CUSTOMER#${customerId}`,
':status': 'pending'
}
});
return result.Items;
};
// TÜM order'ları okur, sonra filtreler
// Tüm order'lar için RCU tüketir, sadece pending'leri döner
// BEST PRACTICE: Composite sort key
// SK formatı: "STATUS#pending#ORDER#456"
const getPendingOrdersOptimized = async (customerId: string) => {
const result = await dynamodb.query({
TableName: 'Orders',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :status)',
ExpressionAttributeValues: {
':pk': `CUSTOMER#${customerId}`,
':status': 'STATUS#pending'
}
});
return result.Items;
};
// SADECE pending order'ları okur
// Sadece eşleşen item'lar için RCU tüketir
Hot Partition’ları Önleme
Her DynamoDB partition 3,000 RCU ve 1,000 WCU destekler. Bu limitleri aşmak throttling’e neden olur.
Hot Partition Senaryoları
// ANTI-PATTERN 1: Low-cardinality partition key
{
PK: "STATUS#active", // Sadece 2-3 unique value
SK: "USER#123"
}
// Tüm active user'lar BİR partition'da
// 3,000 RCU limitini kolayca aşar
// ANTI-PATTERN 2: Celebrity problemi
{
PK: "USER#celebrity",
SK: "FOLLOWER#456"
}
// Bir partition'da milyonlarca follower
// 10GB ve throughput limitlerini aşar
// ANTI-PATTERN 3: Sharding olmadan time-based key
{
PK: "2024-01-15", // Bugünün tüm verisi
SK: "EVENT#123"
}
// Peak saatlerde hot partition
Önleme Stratejisi 1: Write Sharding
// Write'ları dağıtmak için random suffix ekle
const SHARD_COUNT = 10;
const writeWithSharding = async (userId: string, data: any) => {
const shardId = Math.floor(Math.random() * SHARD_COUNT);
await dynamodb.put({
TableName: 'Users',
Item: {
PK: `STATUS#active#${shardId}`, // 10 partition'a dağıtır
SK: `USER#${userId}`,
...data
}
});
};
// Okuma tüm shard'ları query etmeyi gerektirir
const getActiveUsers = async () => {
const promises = [];
for (let i = 0; i < SHARD_COUNT; i++) {
promises.push(
dynamodb.query({
TableName: 'Users',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: {
':pk': `STATUS#active#${i}`
}
})
);
}
const results = await Promise.all(promises);
return results.flatMap(r => r.Items || []);
};
// Write'ları 10 partition'a dağıtır
// Throughput: 10,000 WCU (1,000 * 10)
Önleme Stratejisi 2: Composite High-Cardinality Key’ler
// Low-cardinality'yi high-cardinality ile birleştir
{
PK: `STATUS#active#USER#${userId}`, // User başına unique
SK: "METADATA"
}
// Ya da low-cardinality query'ler için GSI kullan
{
PK: `USER#${userId}`,
SK: "METADATA",
status: "active",
GSI1PK: "STATUS#active", // Status query'leri için GSI
GSI1SK: `USER#${userId}`
}
Önleme Stratejisi 3: Deterministik Sharding
import crypto from 'crypto';
const getShardId = (entityId: string, shardCount: number): number => {
const hash = crypto.createHash('md5').update(entityId).digest('hex');
return parseInt(hash.substring(0, 8), 16) % shardCount;
};
const shardId = getShardId(userId, 10);
const partitionKey = `USERS#${shardId}`;
Bu yaklaşım aynı entity’nin her zaman aynı shard’a gitmesini sağlar, tüm shard’ları query etmeden consistent read’leri mümkün kılar.
Maliyet Optimizasyon Stratejileri
Provisioned vs On-Demand
const pricing = {
provisioned: {
rcu: 'Saat başına $0.00013',
wcu: 'Saat başına $0.00065',
storage: 'GB-ay başına $0.25'
},
onDemand: {
readRequest: 'Milyon read başına $0.155', // Kasım 2024 güncellemesi
writeRequest: 'Milyon write başına $0.78', // Kasım 2024 güncellemesi
storage: 'GB-ay başına $0.25'
}
};
// Örnek: Ayda 100M read, sabit trafik
// Provisioned: ~38.5 RCU * $0.00013 * 730 saat = Ayda $3.65
// On-Demand: 100M / 1M * $0.155 = Ayda $15.50
// Fark: On-demand ile 4.25x daha pahalı
Provisioned Ne Zaman Kullanılmalı:
- Öngörülebilir trafik pattern’leri
- Yüksek volume (günde >1M request)
- 7/24 production uygulamaları
- Budget-conscious senaryolar
- Reserved capacity’ye commit edilebilir (1-yıl %54 veya 3-yıl %77 tasarruf)
On-Demand Ne Zaman Kullanılmalı:
- Öngörülemez trafik
- Bilinmeyen yükü olan yeni uygulamalar
- Development/testing ortamları
- Spiky workload’lar (10x varyans)
- Küçük ölçekli uygulamalar (günde 1M’den az request)
Sparse Index Tasarrufu
// Sparse index olmadan: Tüm 1M user index'te
// Table: 10GB
// ALL projection ile GSI: +10GB
// Toplam: 20GB * $0.25 = Ayda $5 storage
// Sparse index ile: Sadece 100K active user index'te
// Table: 10GB
// Sparse index ile GSI: +1GB (user'ların %10'u)
// Toplam: 11GB * $0.25 = Ayda $2.75 storage
// Tasarruf: %45
Single-Table Maliyet Faydaları
// Multi-table yaklaşımı:
// - Users: 5 RCU, 5 WCU
// - Orders: 10 RCU, 10 WCU
// - Products: 15 RCU, 5 WCU
// - OrderItems: 20 RCU, 20 WCU
// Toplam: 50 RCU, 40 WCU = Ayda $28.47
// Single-table yaklaşımı:
// - MainTable: 30 RCU, 25 WCU
// Toplam: Ayda $15.69
// Tasarruf: %45 + basitleşmiş yönetim
Yaygın Hatalar ve Çözümler
Hata 1: Access Pattern’leri Önce Dokümante Etmemek
Deneyim gösteriyor ki query’leri anlamadan table tasarlamak yeniden tasarımlara yol açıyor:
// YANLIŞ yaklaşım:
// 1. Entity'lere göre table'lar oluştur
// 2. Query'ler çalışmadığında GSI ekle
// 3. 5+ GSI ile bitir, hala inefficient
// DOĞRU yaklaşım:
const accessPatterns = [
'Customer ve tüm order\'larını al',
'Order ve tüm item\'larını al',
'Tarih aralığına göre tüm order\'ları al',
'Email\'e göre customer al',
'Product ve tüm review\'larını al',
'Customer review\'larını al'
];
// TÜM pattern'leri efficiently desteklemek için table ve index'leri tasarla
Hata 2: Throttling için Error Handling Eksikliği
// Production sorunu: Trafik spike sırasında retry logic yok
// Sonuç: %50 error rate
// Çözüm: Exponential backoff
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({
region: 'us-east-1',
maxAttempts: 3,
retryMode: 'adaptive' // Built-in exponential backoff
});
// Daha iyi: Circuit breaker pattern implementasyonu
// En iyi: Hot partition'lardan kaçınacak şekilde tasarla
Hata 3: Item Size Limitlerini Görmezden Gelmek
// Problem: Item başına 400KB limit
// Öğrenme: Bir order'da 500 satır item saklamak limiti aştı
// Çözüm 1: Pagination pattern
{
PK: "ORDER#456",
SK: "ITEMS#PAGE#1",
items: [...] // İlk 100 item
}
{
PK: "ORDER#456",
SK: "ITEMS#PAGE#2",
items: [...] // Sonraki 100 item
}
// Çözüm 2: Bireysel item'lar (tercih edilen)
{
PK: "ORDER#456",
SK: "ITEM#1",
productId: "789",
quantity: 2
}
// Her satır item'ı ayrı DynamoDB item olarak
Hata 4: Table Creation’da LSI Planlamama
Table creation’dan sonra LSI ekleyemezsin. Sonradan ihtiyacın olursa veri migration gerektirir:
// Hemen ihtiyacın olmasa bile LSI'ları önceden planla
const tableDefinition = {
TableName: 'Orders',
KeySchema: [
{ AttributeName: 'PK', KeyType: 'HASH' },
{ AttributeName: 'SK', KeyType: 'RANGE' }
],
LocalSecondaryIndexes: [
{
IndexName: 'LSI-Status',
KeySchema: [
{ AttributeName: 'PK', KeyType: 'HASH' },
{ AttributeName: 'status', KeyType: 'RANGE' }
],
Projection: { ProjectionType: 'ALL' }
}
]
};
// GSI'lar sonradan eklenebilir, LSI'lar eklenemez
Hata 5: Yanlış Capacity Mode
// Production'ı on-demand ile başlattık
// Trafik: Sabit 50 read/sec + 20 write/sec, 7/24
// Aylık maliyet: ~$45/ay (AWS Calculator)
// 129.6M reads + 51.84M writes
// Auto-scaling ile provisioned'a geçtik
// 100 RCU + 40 WCU (eventually consistent reads)
// Aylık maliyet: ~$28/ay
// Tasarruf: ~38% (ayda $17)
// Ders: Öngörülebilir trafik için provisioned capacity daha uygun maliyetli
// Değişken spike'lı trafik için on-demand tercih edilmeli
Single-Table Design NE ZAMAN Kullanılmamalı
Single-table design her zaman doğru seçim değil. Ne zaman kaçınmalı:
Ayrı Table’lar Ne Zaman Kullanılmalı:
- Farklı entity type’ları çok farklı consistency gereksinimlerine sahip
- Ekipte DynamoDB uzmanlığı yok (öğrenme eğrisi velocity’yi etkiler)
- Minimal ilişkili basit CRUD (overhead gerekçelendirilemiyor)
- Ad-hoc reporting ihtiyaçları (data warehouse pattern’leri daha uygun)
- Microservice’lerde service boundary’leri (servis başına ayrı table’lar)
- Farklı entity’lerin access pattern örtüşmesi yok
Rick Houlihan’ın 2024 rehberliği: “Configuration ve operational veriyi single table’da karıştırma. Service boundary’leri boyunca single table sürdürme.”
Temel Çıkarımlar
DynamoDB single-table design ile çalışırken öğrendiklerim:
- Access pattern’ler önce: Schema tasarlamadan önce tüm query’leri dokümante et
- Data locality: İlişkili veriyi aynı partition key ile birlikte sakla
- Scan değil query: Her zaman Query operation’ları için tasarla (100-1000x daha ucuz)
- GSI vs LSI: Esneklik için GSI’lar, strong consistency için LSI’lar
- Hot partition’lar: High-cardinality partition key’ler kullan, gerektiğinde sharding implementasyonu
- DAX ROI: %90+ cache hit rate ile saniyede ~300 req’te break-even
- Maliyet optimizasyonu: Sabit trafik için provisioned mode (6-7x daha ucuz)
- Sparse index’ler: Subset’leri index’leyerek storage’da %50+ tasarruf
- Projection optimizasyonu: ALL yerine INCLUDE veya KEYS_ONLY kullan
- Limitleri bil: Item başına 400KB, LSI partition başına 10GB, partition başına 3,000 RCU
Type-Safe Implementation Kütüphaneleri
Single-table design pattern’lerini TypeScript ile implement ederken, raw AWS SDK yerine type-safe kütüphaneler kullanmak development hızını artırıyor ve runtime hatalarını önlüyor. İki popüler seçenek var:
DynamoDB Toolbox
DynamoDB Toolbox, AWS SDK v3 ile uyumlu, modern bir TypeScript kütüphanesi:
- Type Safety: Entity tanımlarından otomatik TypeScript tipleri
- Schema Validation: Runtime’da veri doğrulama
- Query Builder: Type-safe query ve update expression’ları
- Single-Table Desteği: GSI ve composite key pattern’leri için built-in support
- AWS SDK v3: Son SDK versiyonu ile tam uyumluluk
Raw AWS SDK ile yazdığın karmaşık AttributeValue mapping’leri yerine, temiz ve bakımı kolay entity tanımları kullanabilirsin. Detaylı implementation örnekleri ve production best practice’leri için DynamoDB Toolbox rehberini incele.
OneTable
OneTable, single-table design için özel olarak tasarlanmış alternatif bir kütüphane:
- Schema-Driven: JSON schema ile model tanımlama
- Migration Support: Built-in schema migration desteği
- TypeScript Generation: Schema’dan otomatik type generation
- Developer Experience: Minimal boilerplate, sezgisel API
- Validation: JSON Schema standardı ile güçlü validation
OneTable, özellikle büyük ve karmaşık single-table design’larda schema evolution ve migration ihtiyaçları için güçlü araçlar sunuyor.
Hangisini Seçmeli?
DynamoDB Toolbox tercih et:
- AWS SDK v3’e migrate ediyorsan veya yeni başlıyorsan
- AWS ekosistemi ile daha sıkı entegrasyon istiyorsan
- Daha fazla AWS-native pattern kullanacaksan
- Sitede detaylı rehber mevcut
OneTable tercih et:
- Schema migration’lar sık yapıyorsan
- JSON Schema standardını tercih ediyorsan
- Daha fazla abstraction ve convention over configuration istiyorsan
- Hızlı prototyping yapıyorsan
Her iki kütüphane de production-ready ve aktif olarak maintain ediliyor. Seçim, team preference ve proje gereksinimlerine bağlı.
İlgili Konular
DynamoDB ile çalışmak keşfetmeye değer birkaç alana bağlanıyor:
- DynamoDB Toolbox ile TypeScript Geliştirme - Single-table design için type-safe entity tanımları ve schema validation
- AWS Lambda Guide 101: Cold Start Optimizasyonu - Serverless’ta verimli DynamoDB sorguları
- Serverless’tan CDK’ya Migration Rehberi - DynamoDB tablolarını CDK ile yönetme
- Serverless Architecture Pattern’leri - Event-driven sistemlerde DynamoDB
Single-table design, relational düşünmeden paradigma kaymasını temsil ediyor. Anahtar, önce access pattern’lerini anlamak, efficient query’leri destekleyecek key’ler tasarlamak ve workload’una uygun index’leri seçmek. Basit pattern’lerle başla, performansı ölç ve gerçek kullanıma göre tasarımını geliştir.
İlgili yazılar
Single Table Design uygulamalarında DynamoDB throttling'i önleme ve yönetme stratejileri. Partition key tasarımı, write sharding, kapasite modları, DAX caching, retry pattern'leri ve yüksek throughput sistemler için CloudWatch monitoring konularını kapsar.
Transactional Outbox Pattern'in dağıtık sistemlerdeki dual-write problemini nasıl çözdüğünü, PostgreSQL, DynamoDB ve CDC araçlarıyla pratik implementasyonlarını öğren.
AWS AppSync ile ölçeklenebilir real-time API'ler geliştirmek için kapsamlı bir rehber: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.
Runtime seçimi, veritabanı optimizasyonu, bundle boyutu azaltma ve caching stratejileri ile AWS Lambda'da sub-10ms response süreleri elde edin. Gerçek benchmark'lar ve production deneyimleri dahil.
Event-driven mimarileri CDK'ya taşıma. EventBridge, SQS, SNS entegrasyonları ve pattern'ler.