- SQL anti-pattern’leri, sorguların ve veri pipeline’larının bakımını zorlaştırır ve beklenenden daha yavaş performansa yol açar
- CASE WHEN’in aşırı kullanımı, indeksli kolonlara fonksiyon uygulanması, SELECT *, DISTINCT’in aşırı kullanımı, iç içe view’lar ve alt sorgular, derin bağımlılık yapıları bunun başlıca örnekleridir
- Bu sorunların çoğu, hız ve teslim tarihi baskısı nedeniyle geliştirilen geçici çözümlerden kaynaklanır; uzun vadede hem veri güvenilirliğine hem de geliştirme hızına zarar verir
- Çözüm olarak açık join tanımları, boyut tablolarının kullanımı, gereksiz iç içeliğin kaldırılması, view’ların düzenli olarak temizlenmesi gerekir
- SQL’e basit bir script gibi değil, ekip düzeyinde yönetilen üretim kodu gibi yaklaşmak gerekir; okunabilirliği gözeten ilk tasarım yeniden çalışma ihtiyacını azaltır
Giriş
- Bugün, sık görülen ve etkisi büyük olan bazı SQL anti-pattern örneklerine odaklanılıyor
- Bu sorunlar veri güvenilirliğinin düşmesi, sorgu geliştirme hızının yavaşlaması gibi bir kısır döngü yaratır
- Aşağıdaki liste tüm örnekleri kapsamaz; daha derin bir anlayış için Bill Karwin’in kitabı önerilir
Aşırı karmaşık CASE WHEN ifadeleri
- Büyük sistemlerde durum kodlarını (ör. 1=stokta yok) insanların okuyabileceği hale getirmek için CASE WHEN ifadeleri sık kullanılır
- Dashboard veya raporların hızlı geliştirilmesi için bu CASE WHEN mantığını yalnızca tek bir View’a eklemek, uzun vadede bir anti-pattern sayılır
- Bu yaklaşım, yinelenen mantığın kopyala-yapıştır edilmesine ve yorum farklarına yol açar; tüm sorgu ortamını karmaşıklaştırır
- Çözüm, durum kodlarını dönüştüren ayrı bir boyut tablosu (dimension table) veya ortak view oluşturarak yeniden kullanılabilirliği sağlamaktır
İndeksli kolonlarda fonksiyon kullanımı
WHERE UPPER(name) = 'ABC' örneğinde olduğu gibi indeksli bir kolona fonksiyon uygulandığında indeks verimliliği ortadan kalkar
- SQL Server gibi sistemlerde bu durum gereksiz bir full table scan oluşmasına neden olabilir
- Çözüm, fonksiyon uygulanmış kolonu ayrıca indekslemek veya giriş değerini dönüştürerek sorgu koşulunu sadeleştirmektir
View içinde SELECT * kullanımı
- View geliştirirken SELECT * kullanmak pratik görünebilir, ancak yapı (schema) değiştiğinde view kolayca bozulabilir
- Gereksiz kolonlar da dahil edildiği için istenmeyen bağımlılıklar ve performans sorunları ortaya çıkar; bu yüzden kolonlar açıkça seçilmelidir
DISTINCT ile tekrarları “çözme” alışkanlığı
- Hatalı join’ler nedeniyle yinelenen sonuçlar oluştuğunda, bunu SELECT DISTINCT ile geçici olarak çözmek veri bütünlüğü sorununu gizler
- Asıl neden, join koşullarının eksik olması veya ilişki tanımının (1:1, 1:N vb.) hatalı kurulmasıdır
- Doğru çözüm, join mantığını güçlendirerek ilişki tanımını netleştirmek ve agregasyon ya da raporlama öncesinde ilişki tutarlılığını sağlamaktır
View katmanlarının iç içe binmesi (Excessive View Layer Stacking)
- Birden çok ekip mevcut view’ları yeniden kullanarak sürekli yeni view’lar eklediğinde, bağımlılık zinciri karmaşıklaşır ve performans hızla düşer
- Debugging zorlaşır ve sorguyu genişletmek adeta “arkeolojik kazı” seviyesinde güçleşir
- Dönüştürme mantığını düzenli olarak flatten etmek ve karmaşık işlemleri açık temel view’lara veya tablolara materialize etmek gerekir
Aşırı derin alt sorgular
- 3-4 seviyeden fazla derin iç içe alt sorgular, okunabilirliği azaltır ve debugging’i zorlaştırır
- 5000 satırdan uzun alt sorguların kullanıldığı örnekler bile vardır
- CTE (Common Table Expression) kullanıldığında mantıksal adımları ayırmak kolaylaşır ve sorgunun okunabilirliği (readability) artar
Sonuç
- SQL yüzeyde basit görünse de sistem büyüdükçe karmaşıklığı artma eğilimindedir
- Anti-pattern’lerin çoğu kötü niyetten değil, “hızlı sonuç” alma uğruna verilen tavizlerden (hız, teslim tarihi, geçici çözümler) doğar
- SQL kod gibi yönetildiğinde (sürüm kontrolü, code review, net tasarım) uzun vadede hem verimlilik hem de güvenilirlik birlikte sağlanabilir
- İlk tasarıma birkaç dakika ayırıp açıklık ve tutarlılığı düşünmek, ileride oluşacak yeniden çalışma ihtiyacını ve karmaşayı büyük ölçüde azaltır
5 yorum
Şu an vaktimiz yok, önce acil sorunu çözüp sonra yeniden yazarız dediğimiz şeyler biriktikçe korkunç bir sorgu cehennemine dönüşüyor. Ben de bunlardan epey yaptım. Onları yeniden yazacağımız o “sonra”nın aslında hiç gelmeyeceğini bile bile.
Hımk...
"Çoğu sorun, hız ve teslim tarihi baskısından kaynaklanan geçici çözümlerden doğar"
Ah ah..
Hacker News görüşleri
DISTINCTkullanıldığını gördüğümde, yazarın veri modelini ya da küme teorisini tam olarak anlamadığından, hatta belki ikisini de bilmediğinden şüpheleniyorumDISTINCT, aşırı normalize edilmemiş bir şemanın işareti de olabilir. Örneğin, yinelenen şehir adlarının kaydedilmesini önlemek için ille deaddresses_citiesgibi bir tablo oluşturmaya gerek olmadığını düşünüyorumDISTINCTeklediğimde performansın ciddi biçimde iyileştiği bir durum oldu. Kayıtların benzersizliği garanti edildiğinde sorgu planlayıcısı optimizasyon yapabiliyor gibi görünüyorLIMIT 1eklediğimde de benzer şekilde bunun iyi olmadığı yönünde geri bildirim almıştım. Ama büyük tablolarda (sqlite, mysql, postgresql hepsinde) istediği kaydı bulduktan sonra bile DB tüm tabloyu taramayı sürdürme eğiliminde oluyorSELECT x FROM tsorgusundaDISTINCTifadesini güvenle çıkarıp çıkaramayacağımızı nasıl bilebiliriz diye soruyorum.tşemasındaxiçinPRIMARYya daUNIQUEkısıtı olduğunu görsek bile, biri çıkıp kısa süre sonraUNIQUEkısıtını kaldırabilir. O zaman tekrarlar oluşur ve nedenini anlamaya çalışırız. SQL bir set dili değil, bir bag dilidir. Çalışma anındarel tvexniteliğini bulursa sadece geri döndürür. Tekrarlar oluşabilir, tip değişebilir. Eğer bir Set istiyorsanız mutlakaDISTINCTbelirtmelisiniz. Sorgu planlayıcısı çalışma anındaUNIQUEya daPRIMARYolduğunu görürse deduplication yapmazDISTINCTzorunlu hale geliyor. Özellikle değişken uzunluklu ilişkiler kullanıldığındaDISTINCTolmadan yavaşlıyor ve çok sayıda tekrar oluşuyorDISTINCTolmadan da doğru sorgu yapısını nasıl tasarlayacağınıza dair yaklaşık 9000 karakterlik iki bölümlü bir eğitim yazdımhttps://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
!=ya daNOT IN (...)kullanımı çoğu durumda verimsizdir (ama başka koşullar sonuç kümesini yeterince daraltmışsa sorun olmayabilir). Ayrıca DB’ninnulldeğerlerini nasıl ele aldığını anlamak da önemli.nullile boş string aynı şey mi,null == nullmı, bunlar DB’ye göre değişebilirnullişleme ve indeksleme konusunda, kullandığım DB’lernulldeğerlerini indekslemediği içinWHERE col IS NULLsorguları,colüzerinde indeks olsa bile verimsiz çalışıyor. Gerçekten gerekiyorsacoldeğerinin null olup olmadığını gösterenchar(1)ya dabitsütunu oluşturup o alanı indekslemeyi öneririm!=ya daNOT IN (...)kullanımının neredeyse her zaman verimsiz olduğu söylenmişti; nedenini merak ediyorum. Sağ taraftaki değer sabitse hash table lookup ile genelde verimli olması gerekmez mi? Daha verimli bir alternatif olup olmadığını merak ediyorumDISTINCTgerekiyorsa, primary key tasarımı doğru yapılmamış olabilir. View’ların aşırı katmanlanması da temel tabloların yanlış tasarlandığını gösterir. İyi bir DB modellemesi tüm bu sorunları en baştan önlersargablehale getirmek olduhttps://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
sargablekelimesinin pratikte hangi topluluklarda kullanıldığını merak ediyorum. 20 yılı aşkın süredir SQL ile çalışıyorum ama bunu manual’lerde, Stack Overflow’da ya da HN’de neredeyse hiç görmedim. Hangi DB’de, hangi şirkette ya da hangi açık kaynak topluluğunda daha yaygın kullanılıyor, merak ediyorumsargablesözcüğünün kökenini araştırırken şu StackOverflow yanıtı yardımcı oldu https://dba.stackexchange.com/a/217983sargablekelimesi “Search ARGument ABLE” ifadesinden türetilmiş bir portmanteau’durCASE WHENifadelerinin aşırı kullanımıyla ilgili sorunların çoğu, mantığı tek bir yerde toplamak için UDF (User Defined Function) kullanılarak çözülebilirİndeksli bir sütun üzerinde fonksiyon kullanmak, sorgunun
sargableolmadığının işaretidirDISTINCTaşırı kullanımına alternatif olarak, join’den türeyen fan-out içinden tablo grain’ine uygun şekilde de-dupe etmek için şu tür sorgular yararlı olur: Bazı DB’lerdeQUALIFYözelliği desteklenir ve sorgu çok daha temiz hale gelirsargable açıklaması
Redshift'te QUALIFY
sargableolmayan sorunlar expression index ile kolayca çözülebilir. En azından sqlite için böyle olduğunu düşünüyorumwherekoşullarını ayrı ayrı yazmak, void/iade/iptal gibi çeşitli durumları her seferinde ele almak gerekiyor ve bir değişiklik olduğunda onlarca view/procedure’ı tek tek düzeltmek zorunda kalıyorsunuz. Bizim durumda iç içe view kullanımı çok daha pratikUPPER(name)sütunu üzerine indeks eklemek) en azından MS SQL Server’da en iyi yaklaşım olmadığını düşünüyorum. Diğer DB’lerin bunu destekleyip desteklemediğinden emin değilim ama daha iyi çözüm, doğrudan case-insensitive bir hesaplanmış sütunuCOLLATEile oluşturmaktır (İhtiyaca göre uyarlayın)En önemli şey eksik kalmış.