Veritabanı Veri Modeli

6 tablo · 5 FK zinciri · 10 index · 1 trigger · 2 extension

ER Diyagramı

Tüm tablolar ve aralarındaki ilişkiler. Tablo isimlerine tıklayarak detaylara gidin.

👤 users 🔑 idINT IDENTITY PK emailVARCHAR(255) UNIQUE usernameVARCHAR(128) UNIQUE password_hashVARCHAR NULL timezoneVARCHAR(50) NN avatar_url, created_atTEXT NN, TS 🔐 auth_providers 🔑 idUUID PK 🔗 user_idINT FK → users CASCADE provider_typeVARCHAR(50) provider_idVARCHAR(255) display_nameVARCHAR(255) created_atTIMESTAMP 1:N 📖 stories (15 kolon) 🔑 idINT IDENTITY PK 🔗 user_idINT FK SET NULL like_countINT CHECK≥0 DEFAULT 0 is_publicBOOL NN DEFAULT false llm_modelVARCHAR(60) NN image_modelVARCHAR(60) NN image_frequencyENUM NN cover_image_urlTEXT NN created/updated_atTIMESTAMP NN story_configJSONB NN ⚠️ session_dataJSONB NN ⚠️ summaryTEXT NULL chunk_countINT CHECK≥0 DEFAULT 0 cache_dirtyBOOL NN 1:N 📝 story_chunks 🔑 idUUID PK 🔗 story_idINT FK CASCADE chunk_indexINT (UNIQUE w/story) narrative_textTEXT NOT NULL user_input_*type + value choices, scene_typeJSONB, VARCHAR image_url/promptTEXT embeddingvector(1536) HNSW created_atTIMESTAMP 1:N 🎯 story_decisions 🔑 idUUID PK 🔗 story_idINT FK CASCADE 🔗 chunk_idFK UNIQUE CASCADE decision_textTEXT NOT NULL embeddingvector(1536) created_atTIMESTAMP 1:1 (chunk_id UNIQUE) 1:N 📤 outbox event_type, payload, status idempotency_key, retry_count 1:N Gösterim 🔗= Foreign Key ⚠️= JSONB (TypeBox) 🟣= pgvector embedding

Tablo Detayları

Her tablonun SQL tanımı ve kolon açıklamaları. Tabloya tıklayarak inceleyin.

CREATE TABLE

Kolon Tablosu

JSONB Alanları — Ne Var, Nerede?

PostgreSQL JSONB içeriğini doğrulamaz. Yapısını biz tanımlıyoruz, validasyonu uygulama katmanında TypeBox yapar.

story_config

Index Stratejisi

10 index — her biri belirli bir sorgu pattern'ini hızlandırır.

Index Adı Tablo Tip Kolon(lar) Amacı
idx_stories_user_cursorstoriesB-Tree Compositeuser_id, created_at DESC, id DESCCursor-based pagination (OFFSET yerine)
idx_story_chunks_orderingstory_chunksUNIQUE B-Treestory_id, chunk_indexKronolojik sıralama + uniqueness garantisi
idx_story_chunks_embeddingstory_chunksHNSW Conditionalembedding (WHERE IS NOT NULL)RAG semantic search — ~12ms @ 1M satır
idx_story_decisions_storystory_decisionsB-Tree Compositestory_id, created_atHikayeye ait kararları kronolojik çek
idx_story_decisions_embeddingstory_decisionsHNSW Conditionalembedding (WHERE IS NOT NULL)RAG related_decisions semantic search
idx_stories_cache_dirtystoriesPartial B-Treecache_dirty WHERE trueSadece dirty story'leri tara (Q16 — sessions'tan taşındı)
idx_outbox_pendingoutboxPartial B-Treestatus, created_at WHERE pendingWorker polling — bekleyen event'leri çek
idx_outbox_story_pendingoutboxPartial B-Treestory_id, created_at WHERE pendingPer-story outbox kontrolü (Phase 1)
idx_outbox_cleanupoutboxPartial B-Treestatus, sent_at WHERE sentCleanup cron — eski sent kayıtları sil
idx_outbox_dead_letteroutboxPartial B-Treestatus, created_at WHERE dead_letterMonitoring — müdahale gerektiren event'ler

HNSW Nedir?

Hierarchical Navigable Small World — pgvector'ın approximate nearest neighbor index'i. Exact search yerine ~%99 doğrulukla 70x hızlı arama. m=16 her node 16 komşuya bağlı, ef_construction=64 build kalitesi.

Partial Index Nedir?

Sadece belirli koşulu sağlayan satırlar index'lenir. WHERE status = 'pending' → 1M outbox kaydından sadece ~100 pending olan index'te. Küçük, hızlı, az disk.

Implicit Index'ler

PRIMARY KEY ve UNIQUE constraint'ler otomatik index oluşturur. users.email, users.username, outbox.idempotency_key için ayrı index yazmadık — PG kendisi yapıyor.

CASCADE Silme Zinciri

Kullanıcıyı sil → tüm verisi otomatik temizlenir. Orphan kayıt riski yok.

👤
users

Kullanıcı kaydı siliniyor...

📖
stories

user_id FK CASCADE → tüm hikayeler siliniyor...

📝
story_chunks

story_id FK CASCADE → tüm chunk'lar siliniyor...

🎯
story_decisions

chunk_id FK CASCADE + story_id FK CASCADE → tüm kararlar siliniyor...

📤
outbox

story_id FK CASCADE → event'ler siliniyor...

⚠️ DBA Uyarısı: Büyük veri setlerinde CASCADE silme tablo kilitlemeye neden olabilir. Üretimde soft-delete (status='deleted') tercih edilebilir.

Code-First — Drizzle ORM Tanımları

SQL yazmak yerine TypeScript ile tanımlıyoruz. Drizzle Kit otomatik migration SQL'i üretir.

schema.ts

Trigger — updated_at Otomatik Güncelleme

1 tabloda (stories) UPDATE yapıldığında updated_at otomatik NOW() olur.

trigger.sql
-- Fonksiyon: updated_at kolonunu NOW() ile günceller CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- stories tablosuna bağla (BEFORE UPDATE) CREATE TRIGGER trg_stories_updated_at BEFORE UPDATE ON stories FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ❌ users'ta YOK → updated_at kolonu kaldırıldı (Q16) -- ❌ sessions tablosu kaldırıldı (Q16) -- ❌ story_chunks ve story_decisions'ta YOK -- → Bu tablolar immutable (yazıldıktan sonra değişmez) -- ❌ outbox'ta YOK -- → status değişiyor ama "ne zaman güncellendi" önemli değil, sent_at var

Neden BEFORE UPDATE? AFTER UPDATE'ten farklı olarak, BEFORE trigger satır yazılmadan önce değişiklik yapabilir. Performans farkı yok ama mantıksal olarak daha doğru — updated_at değerini "yazılacak verinin" parçası yapıyoruz.