Jeffrey Ullman'ın temel fikirlerinin modern veritabanlarını nasıl güçlendirdiği: ilişkisel cebir, optimizasyon kuralları, join'ler ve sistemlerin ölçeklenmesine yardımcı olan derleyici tarzı planlama.

SQL yazan, gösterge tabloları oluşturan veya yavaş bir sorguyu inceleyen çoğu kişi, adını hiç duymamış olsa bile Jeffrey Ullman’ın çalışmalarından faydalanmıştır. Ullman, veritabanlarının veriyi nasıl tanımladığı, sorguları nasıl anladığı ve bunları verimli şekilde nasıl çalıştırdığı konusunda araştırmaları ve ders kitaplarıyla önemli katkılar yapmış bir bilgisayar bilimci ve eğitimcidir.
Bir veritabanı motoru SQL’inizi hızlı çalıştırılabilir bir şeye dönüştürdüğünde, bunu hem kesin hem de uyarlanabilir fikirler üzerine kurar. Ullman, sorguların anlamını formalize etmeye yardımcı oldu (sistemin güvenle yeniden yazabilmesi için) ve veritabanı düşüncesini derleyici düşüncesiyle bağladı (sorgunun ayrıştırılması, optimize edilmesi ve yürütülebilir adımlara çevrilmesi için).
Bu etki görünür bir buton ya da bulut konsolunda bir özellik olarak ortaya çıkmaz. Bunun yerine şunlarda kendini gösterir:
JOIN’i yeniden yazdığınızda hızlı çalışan sorgularBu yazı, Ullman’ın temel fikirlerini pratikte en çok önem taşıyan veritabanı içyapılarına bir rehber tur olarak kullanır: ilişkisel cebirin SQL’in altında nasıl durduğu, sorgu yeniden yazımlarının anlamı nasıl koruduğu, maliyet tabanlı optimizer’ların neden belli seçimleri yaptığı ve join algoritmalarının bir işin saniyeler içinde mi yoksa saatler içinde mi tamamlanacağını nasıl belirlediği.
Ayrıca ayrıştırma, yeniden yazma ve planlama gibi derleyici benzeri birkaç kavramı da dahil edeceğiz; çünkü veritabanı motorları birçok kişinin sandığından daha çok sofistike derleyicilere benzer davranır.
Kısa bir söz: tartışmayı doğru tutacağız ama ağır kanıtlardan kaçınacağız. Amaç, bir dahaki performans, ölçekleme ya da kafa karıştıran sorgu davranışı ortaya çıktığında iş yerinizde uygulayabileceğiniz zihinsel modeller vermektir.
Eğer hiç SQL yazdıysanız ve sorgunuzun “tek bir anlamı olduğunu” varsaydınız, büyük olasılıkla Jeffrey Ullman’ın popülerleştirdiği ve formalize ettiği fikirlere dayanıyorsunuz: veri için temiz bir model ve bir sorgunun ne istediğini kesin biçimde tanımlamanın yolları.
Özünde ilişkisel model veriyi tablolar (relation) olarak ele alır. Her tablonun satırları (tuple) ve sütunları (attribute) vardır. Şimdi bariz görünüyor olabilir, ama önemli olan disiplin şudur:
Bu çerçeve, el yordamıyla konuşmak yerine doğruluk ve performans hakkında mantık yürütmeyi mümkün kılar. Bir tablonun neyi temsil ettiğini ve satırların nasıl tanımlandığını bildiğinizde, join’lerin ne yapması gerektiğini, tekrarların ne anlama geldiğini ve belirli filtrelerin neden sonuçları değiştirdiğini öngörebilirsiniz.
Ullman’ın öğretilerinde sıkça kullanılan ilişkisel cebir, bir çeşit sorgu hesap makinesi gibidir: seçme, projeksiyon, join, union, difference gibi bir dizi temel operasyonla ne istediğinizi ifade edersiniz.
Neden SQL ile çalışırken önemli: veritabanları SQL’i cebirsel bir forma çevirir ve sonra onu eşdeğer başka bir forma yeniden yazar. Görünüşte farklı iki sorgu cebirsel olarak aynı olabilir—işte optimizer’ların join’leri yeniden sıralayabilmesi, filtreleri öne itebilmesi veya gereksiz işi kaldırabilmesi bu sayede mümkün olur.
SQL büyük oranda “ne”dir, ama motorlar genellikle optimizasyon için cebirsel “nasıl”ı kullanır.
SQL dialektleri değişir (Postgres vs. Snowflake vs. MySQL), ancak temel ilkeler değişmez. Anahtarları, ilişkileri ve cebirsel eşdeğerliği anlamak, bir sorgunun mantıksal olarak yanlış mı yoksa sadece yavaş mı olduğunu, ve hangi değişikliklerin anlamı koruyacağını gösterebilir.
İlişkisel cebir, SQL’in “altındaki matematik”tir: istediğiniz sonucu tanımlayan küçük operatör seti. Jeffrey Ullman’ın çalışmaları bu operatör bakışını netleştirmeye ve öğretilebilir kılmaya yardımcı oldu—hala çoğu optimizer’ın kullandığı zihinsel model budur.
Bir veritabanı sorgusu birkaç yapı taşının boru hattı olarak ifade edilebilir:
WHERE fikri)SELECT col1, col2 fikri)JOIN ... ON ...)UNION)EXCEPT gibi)Kümenin küçük olması, doğruluk üzerine mantık yürütmeyi kolaylaştırır: iki cebirsel ifade eşdeğerse, herhangi bir geçerli veritabanı durumunda aynı tabloyu döndürürler.
Tanıdık bir sorguyu ele alalım:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
Kavramsal olarak bu şudur:
önce customers ve orders’ın bir joini: customers ⋈ orders
yalnızca 100’den büyük olan siparişleri seç: σ(o.total > 100)(...)
istediğiniz tek sütunu projekte et: π(c.name)(...)
Bu her motorun kullandığı tam dahili notasyon olmayabilir, ama doğru fikir budur: SQL bir operatör ağacına dönüşür.
Birçok farklı ağaç aynı sonucu verebilir. Örneğin, filtreler genellikle daha erken uygulanabilir (büyük bir join’den önce σ uygula) ve projeksiyonlar kullanılmayan sütunları daha erken atabilir (π).
Bu eşdeğerlik kuralları veritabanının sorgunuzu daha ucuz bir plana yeniden yazmasını anlamı değiştirmeden sağlar. Sorguları cebirsel olarak gördüğünüzde, “optimizasyon” sihir olmaktan çıkar ve kural-odaklı güvenli bir yeniden şekillendirme haline gelir.
SQL yazdığınızda, veritabanı onu "yazıldığı gibi" yürütmez. İfadenizi bir sorgu planına çevirir: yapılacak işin yapılandırılmış bir temsili.\
İyi bir zihinsel model, operatör ağacıdır. Yapraklar tabloları veya index’leri okur; iç düğümler satırları dönüştürür ve birleştirir. Yaygın operatörler arasında scan, filter (selection), project, join, group/aggregate ve sort bulunur.
Veritabanları tipik olarak planlamayı iki katmana ayırır:
Ullman’ın etkisi, anlamı koruyan dönüşümlere verilen vurguda kendini gösterir: mantıksal planı birçok şekilde yeniden düzenleyin ama cevabı değiştirmeyin, sonra verimli fiziksel stratejiyi seçin.
Final yürütme yaklaşımını seçmeden önce optimizer’lar cebirsel “temizlik” kuralları uygular. Bu yeniden yazımlar sonucu değiştirmez; gereksiz işi azaltır.
Yaygın örnekler:
Kullanıcıların bir ülkedeki siparişlerini almak istiyorsunuz:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Naif bir yorum tüm kullanıcıları tüm siparişlerle birleştirip sonra Kanada filtresi uygulayabilir. Anlamı bozmadan filtreyi öne itmek join’in daha az satırla çalışmasını sağlar:
country = 'CA' ile kullanıcıları filtreleorder_id ve total projekte etPlan terimleriyle optimizer şöyle bir dönüşüm yapmaya çalışır:
Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)
yerine daha yakın bir şeye:
Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)
Aynı cevap. Daha az iş.
Bu yeniden yazımlar, siz onları yazmadığınız için kolayca gözden kaçabilir—yine de aynı SQL’in bir veritabanında hızlı, diğerinde yavaş çalışmasının başlıca nedenlerinden biridir.
Bir SQL sorgusu çalıştırdığınızda, veritabanı aynı sonucu veren birden fazla yolu dikkate alır ve sonra en ucuz olması beklenenini seçer. Bu sürece maliyet tabanlı optimizasyon denir—Ullman tarzı teorinin günlük performansta en pratik şekilde göründüğü yerlerden biridir.
Maliyet modeli, optimizer’ın alternatif planları karşılaştırmak için kullandığı bir puanlama sistemidir. Çoğu motor maliyeti şu çekirdek kaynaklarla tahmin eder:
Modelin mükemmel olması gerekmez; yeterince sık yönlendirici olacak şekilde doğru olması yeterlidir.
Planları puanlamadan önce optimizer her adımda şu soruyu sorar: bu adım kaç satır üretecek? Buna cardinality estimation denir.
Eğer WHERE country = 'CA' filtreliyorsanız, motor tablonun hangi oranının eşleştiğini tahmin eder. Müşterileri siparişlere join ediyorsanız, eşleşecek kaç çift olduğunu tahmin eder. Bu satır sayısı tahminleri, index taramayı mı yoksa tam taramayı mı seçeceğini, hash join mi nested loop mu olacağını ya da bir sıralamanın küçük mü yoksa devasa mı olacağını belirler.
Optimizer’ın tahminleri istatistikler tarafından yönlendirilir: sayımlar, değer dağılımları, null oranları ve bazen sütunlar arası korelasyonlar.
İstatistikler güncel değilse veya eksikse, motor satır sayısını katlarca yanlış tahmin edebilir. Kağıt üzerinde ucuz görünen bir plan gerçekte pahalı olabilir—klasik belirtiler arasında veri büyüdükten sonra ani yavaşlamalar, “rastgele” plan değişiklikleri veya beklenmedik disk spill’leri yer alır.
Daha iyi tahminler genellikle daha fazla iş gerektirir: daha detaylı istatistikler, örneklem alma veya daha fazla aday planı keşfetme. Ancak planlama işlemi de zaman alır, özellikle karmaşık sorgular için.
Bu yüzden optimizer’lar iki hedefi dengeler:
EXPLAIN çıktısını yorumlarken bu takası anlamak yardımcı olur: optimizer zekice davranmaya çalışmıyor—kısıtlı bilgiler altında tahmin edilebilir şekilde doğru olmaya çalışıyor.
Ullman’ın çalışmaları, SQL’in “koşulduğu”ndan çok çevirildiği fikrini popülerleştirdi. Bu, join’lerde en açık şekilde görülür. Aynı satırları döndüren iki sorgu, motorun seçtiği join algoritması ve join sırası nedeniyle çalışma süresi açısından çok farklı olabilir.
Nested loop join fikri basittir: soldaki her satır için sağdaki eşleşen satırları bul. Sol taraf küçük ve sağ tarafta kullanılabilir bir index varsa hızlı olabilir.
Hash join bir girişten (genellikle daha küçük olandan) bir hash tablosu oluşturur ve diğer tarafla probe eder. Eşitlik koşullarında (ör. A.id = B.id) büyük, sırasız girdiler için iyidir, ama bellek ister; diske taşma avantajı yok edebilir.
Merge join iki girişi sıralı olarak yürür. Her iki taraf da zaten sıralıysa (veya ucuzca sıralanabiliyorsa) iyi çalışır; örneğin index’ler join anahtarına göre sıralı satırlar verebiliyorsa.
Üç veya daha fazla tablo olduğunda olası join sırası sayısı patlar. İlk iki büyük tabloyu birleştirmek büyük bir ara sonuç üretebilir ve her şeyi yavaşlatır. Daha iyi bir sıra genellikle en seçici filtreden başlayıp dışarıya doğru join etmektir; böylece ara sonuçlar küçük kalır.
Index’ler sadece aramaları hızlandırmaz—bazı join stratejilerini mümkün kılar. Join anahtarında bir index, pahalı bir nested loop’u hızlı bir “satır başına seek” modeline çevirebilir. Öte yandan, eksik veya kullanılamayan index’ler motoru hash join’lere veya merge join için büyük sıralamalara zorlayabilir.
Veritabanları sadece "SQL çalıştırmaz." Onu derlerler. Ullman’ın etkisi veritabanı teorisi ile derleyici düşüncesinin kesişimine kadar uzanır ve bu bağlantı, sorgu motorlarının neden programlama dili araç zincirlerine benzediğini açıklar: çevirmek, yeniden yazmak ve çalıştırmadan önce optimize etmek.
Sorguyu gönderdiğinizde ilk adım bir derleyicinin önden ayrıştırmayla (front end) yaptığına benzer. Motor anahtar kelimeleri ve tanımlayıcıları token’lara ayırır, grameri kontrol eder ve bir parse tree (çoğu zaman basitleştirilmiş bir abstract syntax tree) oluşturur. Burada eksik virgüller, belirsiz sütun adları ve geçersiz grouping kuralları gibi temel hatalar yakalanır.
Yararlı bir zihinsel model: SQL, “program”u döngüler yerine veri ilişkilerini tanımlayan bir programlama dilidir.
Derleyiciler sözdizimini ara temsile çevirir. Veritabanları da benzer şekilde SQL sözdizimini mantıksal operatörlere çevirir:
GROUP BY)Bu mantıksal form SQL metninden daha çok ilişkisel cebire yakındır; bu da anlam ve eşdeğerlik üzerinde düşünmeyi kolaylaştırır.
Derleyici optimizasyonları program sonuçlarını aynı tutarken yürütmeyi ucuzlatır. Veritabanı optimizer’ları da benzer şekilde davranır, aşağıdaki tür kural sistemlerini kullanarak:
Bu, derleyicilerdeki “dead code elimination” felsefesinin veritabanı versiyonudur: aynı felsefe ama farklı teknikler—anlamı koru, maliyeti azalt.
Sorgunuz yavaşsa yalnızca SQL’e bakmayın. EXPLAIN çıktısını, derleyici çıktısını inceler gibi okuyun. Bir plan size motorun gerçekten ne seçtiğini söyler: join sırası, index kullanımı ve zamanın nerede harcandığı.
Pratik çıkarım: EXPLAIN çıktısını performansın “assembly listesi” gibi okumayı öğrenin. Bu, ayarlamayı tahmine dayalı işten kanıta dayalı hata ayıklamaya dönüştürür. Daha fazla uygulama alışkanlığı için practical-query-optimization-habits başlıklı yazıyı inceleyin.
İyi sorgu performansı genellikle SQL yazmadan önce başlar. Ullman’ın şema tasarım teorisi (özellikle normalizasyon), veriyi doğru, öngörülebilir ve büyüdükçe verimli tutmak üzerine odaklanır.
Normalizasyonun hedefleri şunlardır:
Bu doğruluk kazanımları daha sonra performans kazançlarına dönüşür: daha az tekrar eden alan, daha küçük index’ler ve daha az pahalı güncellemeler.
Kanıtları ezberlemeye gerek yok, fikirleri kullanmak yeterli:
Denormalizasyon şu durumlarda akıllıca olabilir:
Anahtar nokta denormalize ederken kasıtlı olmak ve çoğaltmaları senkronize tutma sürecine sahip olmaktır.
Şema tasarımı optimizer’ın neler yapabileceğini şekillendirir. Açık anahtarlar ve foreign key’ler daha iyi join stratejilerine, daha güvenli yeniden yazımlara ve daha doğru satır tahminlerine imkan tanır. Fazla tekrar ise index’leri şişirir ve yazmaları yavaşlatır; çok değerli sütunlar ise etkili predikatları engeller. Veri hacmi büyüdükçe, bu erken modelleme kararları genellikle tek bir sorgunun mikro-optimizasyonlarından daha önem kazanır.
Bir sistem “ölçeklendiğinde”, genellikle sadece daha büyük makineler eklemek değildir. Zor kısım, aynı sorgu anlamını korurken motorun çok farklı bir fiziksel strateji seçmesi gerektiğidir. Ullman’ın eşdeğerliklere verdiği vurgu, bu strateji değişikliklerinin sonucu değiştirmeden yapılmasını sağlar.
Küçük boyutlarda birçok plan “iş görür”. Ölçekte, bir tabloyu taramak, bir index kullanmak veya önhesaplanmış bir sonucu kullanmak arasındaki fark saniyeler ile saatler arasında olabilir. Teorik taraf önemlidir çünkü optimizer’ın güvenli bir yeniden yazım kümesine (ör. filtreleri öne itme, join’leri yeniden sıralama) sahip olması gerekir—bunlar sonucu değiştirmeden işi köklü şekilde değiştirir.
Partitioning (tarihe, müşteriye, bölgeye göre) tek bir mantıksal tabloyu birçok fiziksel parçaya böler. Bu planlamayı etkiler:
SQL metni değişmeyebilir, ama en iyi plan artık satırların nerede olduğuna bağlıdır.
Materialized view’ler temel olarak "kaydedilmiş alt ifadeler"dir. Motor, sorgunuzun saklanan bir sonuçla eşleştiğini (veya eşdeğer hale getirilebileceğini) ispatlayabilirse, pahalı işleri—tekrarlanan join ve agregasyonları—hızlı bir aramayla değiştirebilir. Bu, ilişkisel cebir düşüncesinin pratik uygulamasıdır: eşdeğerliği tanı, sonra yeniden kullan.
Cache tekrar eden okumaları hızlandırır, ama çok fazla veriyi taramak zorunda olan veya devasa ara sonuçlar üreten bir sorguyu kurtaramaz. Ölçek sorunları ortaya çıktığında genellikle çözüm: dokunulan veri miktarını azaltmak (düzen/partitioning), tekrarlayan hesaplamayı azaltmak (materialized view) veya planı değiştirmektir—sadece "cache ekle" değil.
Ullman’ın etkisi basit bir zihniyette görünür: yavaş bir sorguyu, veritabanının yeniden yazmaya serbest olduğu bir niyet ifadesi olarak ele alın ve sonra motorun gerçekten ne yapmaya karar verdiğini doğrulayın. Teorisyen olmanıza gerek yok—sadece tekrarlanabilir bir rutin gerekli.
Zamanı genellikle domine eden parçalardan başlayın:
Sadece bir şey yapacaksanız, satır sayısının patladığı ilk operatörü belirleyin. Genellikle temel neden odur.
Yazması kolay ama maliyeti yüksek olanlar:
WHERE LOWER(email) = ... index kullanımını engelleyebilir (normalize sütun veya destekliyorsa fonksiyonel index kullanın).İlişkisel cebir iki pratik hareketi teşvik eder:
WHERE koşullarını join’lerden önce uygulayın.İyi bir hipotez şöyle seslenir: “Bu join pahalı çünkü çok fazla satır birleştiriyoruz; ordersı önce son 30 güne filtrelersek join girişi küçülür.”
Basit bir karar kuralı kullanın:
EXPLAIN kaçınılabilir işi gösteriyorsa (gereksiz join’ler, geç filtreleme, non-sargable predicate’ler).Amaç “zeki SQL” değil. Amaç öngörülebilir, daha küçük ara sonuçlar — Ullman’ın eşdeğerlik fikirlerinin tespit etmeyi kolaylaştırdığı türden iyileştirmeler.
Bu kavramlar sadece DBA’lar için değil. Bir uygulama yayımlıyorsanız, şema şekli, anahtar seçimleri, sorgu kalıpları ve veri erişim katmanı üzerinden veritabanı ve sorgu planlama kararları alıyorsunuz.
Eğer bir vibe-coding iş akışı kullanıyorsanız (örneğin sohbet arayüzünden bir React + Go + PostgreSQL uygulaması üretmek gibi) Koder.ai içinde Ullman benzeri zihinsel modeller pratik bir güven ağı sağlar: üretilen şemayı anahtarlar ve ilişkiler açısından gözden geçirebilir, uygulamanızın dayandığı sorguları inceleyebilir ve üretime geçmeden önce EXPLAIN ile performansı doğrulayabilirsiniz. "Sorgu niyeti → plan → düzeltme" döngüsünde ne kadar hızlı iterasyon yaparsanız, hızlandırılmış geliştirmeden o kadar çok değer elde edersiniz.
Teori ayrı bir hobi olarak çalışmanıza gerek yok. Ullman temellerinden faydalanmanın en hızlı yolu, sorgu planlarını güvenle okuyacak kadar öğrenmek ve sonra bunu kendi veritabanınız üzerinde pratik etmektir.
Aşağıdaki kitap ve ders konularını arayabilirsiniz (bağlantısız, sadece yaygın başlangıç noktaları):
Küçük başlayın ve her adımı gözlemleyebileceğiniz bir şeye bağlayın:
2–3 gerçek sorgu seçin ve yineleyin:
IN yerine EXISTS kullanma, predikatleri öne itme, gereksiz sütunları kaldırma, sonuçları karşılaştırma.Net, plan-temelli bir dil kullanın:
Ullman’ın temellerinin pratik getirisi budur: plan-temelli bir ortak dil sayesinde performansı tahmine dayalı işten açıklamalı kanıt temelli konuşmaya taşırsınız.
Jeffrey Ullman, veritabanlarının sorgu anlamını nasıl temsil ettiğini ve sorguları daha hızlı eşdeğerlerine nasıl dönüştürebileceğini formalize etmeye yardımcı oldu. Bir motor sorguyu yeniden yazdığında, join sırasını değiştirdiğinde veya farklı bir yürütme planı seçtiğinde bu temel bilgiler devreye girer ve aynı sonuç kümesini garantiler.
İlişkisel cebir, sorgu sonuçlarını kesin olarak tanımlayan küçük bir operatör kümesidir (select, project, join, union, difference). Motorlar genellikle SQL’i cebir-benzeri bir operatör ağacına çevirir; böylece optimizer’lar WHERE gibi filtreleri daha erken uygulamak gibi eşdeğerlik kurallarını güvenle uygulayabilir.
Çünkü optimizasyon, yeniden yazılmış sorgunun aynı sonuçları döndürdüğünü kanıtlamaya dayanır. Eşdeğerlik kuralları optimizer’ın şunları yapmasına izin verir:
WHERE filtrelerini join’lerden önce itmekBu değişiklikler işi dramatik biçimde azaltabilir, ama sonucu değiştirmez.
Mantıksal plan, hangi operasyonların gerektiğini (filtre, join, aggregate) depoladığı soyut bir tarif sunar. Fiziksel plan ise bunların nasıl çalıştırılacağını seçer (index scan vs full scan, hash join vs nested loop, paralelleştirme, sıralama stratejileri). Çoğu performans farkı fiziksel seçimlerden kaynaklanır; bu seçimler mantıksal yeniden yazımlarla mümkün olur.
Maliyet tabanlı optimizasyon, birden fazla geçerli planı değerlendirir ve en düşük tahmini maliyete sahip olanını seçer. Maliyetler genellikle işlenen satır sayısı, I/O, CPU ve bellek (ör. hash veya sort’un diske taşınıp taşınmadığı) gibi pratik faktörlerle belirlenir.
Cardinality estimation, optimizer’ın “bu adımdan kaç satır çıkacak?” sorusuna verdiği cevaptır. Bu tahminler join sırasını, join türünü ve index kullanımını etkiler. Tahminler yanlış olduğunda (çoğunlukla eski veya eksik istatistikler yüzünden) ani yavaşlamalar, büyük spill’ler veya beklenmedik plan değişiklikleri görülebilir.
Aşağıdaki yüksek sinyalli göstergelere bakın:
Planı bir derleyicinin ürettiği kod gibi değerlendirin: motorun gerçekte ne seçtiğini gösterir.
Normalizasyon, tekrarları azaltır ve güncelleme anomalilerini önler; bu da çoğu zaman daha küçük tablolar, daha küçük index’ler ve daha güvenilir join’ler demektir. Denormalizasyon, analitik veya yoğun okuma gerektiren durumlarda kabul edilebilir, ancak kontrollü olmalı (yenileme kuralları, bilinen fazlalıklar) ki doğruluk zamanla bozulmasın.
Özetle, ölçek genellikle sorgunun fiziksel stratejisinin değişmesini gerektirir; sorgu anlamı aynı kalırken motor tamamen farklı bir yürütme seçebilir. Yaygın araçlar:
Cache fayda sağlar ama çok fazla veriyi taramak zorunda olan veya büyük ara join’ler üreten sorguyu tek başına düzeltemez.