AI tarafından üretilen API'ler için Go + Postgres performans kılavuzu: bağlantı havuzları, EXPLAIN ile sorgu planları, doğru indeksler, güvenli sayfalama ve hızlı JSON şekillendirme.

AI tarafından üretilen API'ler başlangıç testlerinde hızlı gelebilir. Bir endpoint'e birkaç kez istek yaparsınız, veri seti küçüktür ve istekler tek tek gelir. Gerçek trafik gelince: karışık endpoint'ler, ani yük artışları, daha soğuk önbellekler ve beklenenden fazla satır olur. Aynı kod rastgele yavaşlamaya başlar, sanki bir şey kırılmış gibi değil ama deneyim bozulur.
Yavaşlık genellikle birkaç şekilde kendini gösterir: gecikme zirveleri (çoğu istek iyi, bazıları 5x–50x daha uzun sürer), zaman aşımı (küçük bir yüzdelik hata verir) veya CPU yüksek çalışması (Postgres sorgu CPU'su veya Go CPU'su JSON, gorutinler, logging ve yeniden denemelerden dolayı).
Yaygın bir senaryo, esnek filtreli bir liste endpoint'idir ve büyük bir JSON yanıtı döner. Test veritabanında birkaç bin satır taranır ve hızlı biter. Prodüksiyonda ise birkaç milyon satır taranır, bunlar sıralanır ve ancak sonra LIMIT uygulanır. API hâlâ "çalışır", ama p95 gecikmesi patlar ve bazı istekler ani artışlarda zaman aşımına uğrar.
Veritabanı yavaşlığını uygulama yavaşlığından ayırmak için zihniyeti basit tutun.
Veritabanı yavaşsa, Go handler'ınız zamanının çoğunu sorgunun bitmesini bekleyerek geçirir. Birçok isteğin "uçuşta" sıkıştığını ve Go CPU'nun normal göründüğünü görebilirsiniz.
Uygulama yavaşsa, sorgu hızlı biter ama sorgudan sonra zaman harcanır: büyük cevap nesneleri oluşturma, JSON marshal etme, satır başına ekstra sorgular veya istekte çok fazla işlem yapma. Go CPU ve bellek artar, ve gecikme yanıt boyutuyla birlikte büyür.
Lansman öncesi için "yeterince iyi" performans mükemmel olmak zorunda değil. Birçok CRUD endpoint'i için hedef: stabil p95 gecikmesi (ortalama değil), ani yüklerde öngörülebilir davranış ve beklenen zirvede zaman aşımı olmaması. Amaç basit: veri ve trafik arttığında sürpriz yavaş istekler olmasın ve bir şey saptığında net sinyaller olsun.
Herhangi bir ayar yapmadan önce "iyi"nin ne olduğunu kararlaştırın. Bir temel olmadan, ayar değiştirip iyileşme olup olmadığını veya darboğazı sadece başka yana taşıyıp taşımadığınızı bilmeden saatler harcayabilirsiniz.
Genellikle hikâyeyi anlatan üç sayı vardır:
p95 "kötü gün" metriğidir. Eğer p95 yüksek ama ortalama iyiyse, küçük bir istek grubu çok fazla iş yapıyor, kilitleniyor veya yavaş planlar tetikliyor demektir.
Yavaş sorguları erken görünür kılın. Postgres'te, lansman öncesi test için düşük bir eşik ile slow query logging'i açın (ör. 100–200 ms) ve tam ifadeyi loglayın ki SQL istemcisine kopyalayabilesiniz. Bu geçici olsun. Üretimde her yavaş sorguyu loglamak hızla gürültü oluşturur.
Sonra, tek bir "hello world" rotası yerine gerçeğe yakın isteklerle test edin. Küçük bir set yeterlidir, yeter ki kullanıcıların yapacağı şeylere uysun: filtreli ve sıralamalı bir liste çağrısı, birkaç JOIN içeren detay sayfası, doğrulama ile create/update ve kısmi eşleşmeli arama tarzı sorgu.
Eğer endpoint'leri bir şemadan üretiyorsanız (ör. Koder.ai gibi bir araçla), aynı tutarlı girdilerle aynı birkaç isteği tekrar çalıştırın. Bu, indeksler, sayfalama ayarları ve sorgu yeniden yazımlarının ölçülmesini kolaylaştırır.
Son olarak, yüksek sesle söyleyebileceğiniz bir hedef seçin. Örnek: "Çoğu istek 50 eşzamanlı kullanıcıda p95 altında 200 ms kalsın ve hatalar %0.5'in altında olsun." Kesin rakam ürününüze bağlıdır, ama net bir hedef sonsuz uğraşmayı engeller.
Bir bağlantı havuzu, açık veritabanı bağlantılarının sayısını sınırlar ve bunları yeniden kullanır. Havuz yoksa, her istek yeni bir bağlantı açabilir ve Postgres oturum yönetimi yerine sorgu çalıştırmak yerine zaman ve bellek harcar.
Amaç, Postgres'i yararlı iş yaparken meşgul tutmaktır; çok fazla bağlantı arasında bağlam değiştirmek değil. Bu genellikle ilk anlamlı kazançtır, özellikle chatty endpoint'lere kolayca dönüşebilen AI-üretimli API'ler için.
Go'da genellikle max open connections, max idle connections ve connection lifetime ayarlanır. Birçok küçük API için güvenli başlangıç: CPU çekirdeklerinizin küçük bir katı (çoğunlukla toplam 5–20 bağlantı), benzer sayıda idle bağlantı ve bağlantıları periyodik olarak yenilemek (ör. her 30–60 dakika).
Birden fazla API örneği çalıştırıyorsanız havuzun çarpıldığına dikkat edin. 10 örnekte her biri 20 bağlantılık bir havuz 200 bağlantı yapar ve bu takımların beklenmedik şekilde connection limitlerine takılmasına yol açar.
Havuz problemleri SQL yavaşlığından farklı hissedilir.
Havuz çok küçükse, istekler Postgres'e ulaşmadan önce bekler. Gecikme zirveleri olur, fakat veritabanı CPU ve sorgu zamanları normal görünebilir.
Havuz çok büyükse, Postgres aşırı yüklü görünür: çok sayıda aktif oturum, bellek baskısı ve endpoint'ler arasında düzensiz gecikme.
İkiye ayrılmış zamanlama yapmak hızlı bir ayırıcıdır: bağlantı alma için geçen süre vs sorguyu yürütme süresi. Eğer zamanın çoğu "beklemede" ise havuz sorundur. Eğer zamanın çoğu "sorguda" ise SQL ve indekslere odaklanın.
Hızlı kontroller:
max_connections'a ne kadar yaklaştığınızı izleyin.pgxpool kullanıyorsanız Postgres-odaklı bir havuz, net istatistikler ve Postgres davranışı için iyi varsayılanlar alırsınız. database/sql kullanıyorsanız veritabanları arası ortak bir arayüz elde edersiniz, ancak havuz ayarlarını ve sürücü davranışını açıkça ayarlamanız gerekir.
Pratik bir kural: Tamamen Postgres'e bağlıysanız ve doğrudan kontrol istiyorsanız pgxpool genellikle daha basittir. Eğer database/sql bekleyen kütüphanelere bağımlıysanız, onunla devam edin, havuzu açıkça ayarlayın ve beklemeleri ölçün.
Örnek: bir sipariş listeleyen endpoint 20 ms çalışırken 100 eşzamanlı kullanıcı altında 2 s'ye fırlıyorsa ve loglar 1.9 s'in bağlantı beklemede geçtiğini gösteriyorsa, sorgu iyileştirme havuz ve toplam Postgres bağlantıları doğru boyuta gelene kadar yardımcı olmayacaktır.
Bir endpoint yavaş hissettiriyorsa, Postgres'in gerçekten ne yaptığını kontrol edin. EXPLAIN'ın hızlı bir okunması çoğu zaman dakikalar içinde çözümü gösterir.
API'nizin gönderdiği tam SQL üzerinde bunu çalıştırın:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;
Birkaç satır en fazla önem taşır. Postgres'in seçtiği üst düğüme bakın ve alttaki toplam süreleri kontrol edin. Tahmin edilen satır ile gerçek satır arasındaki büyük farklar genellikle planlayıcının yanlış tahmin ettiğini gösterir.
Eğer Index Scan veya Index Only Scan görürseniz, Postgres bir indeksi kullanıyor demektir; genelde iyidir. Bitmap Heap Scan orta büyüklükteki eşleşmeler için uygun olabilir. Seq Scan tüm tabloyu okuduğunu gösterir; tablo küçükse veya neredeyse her satır eşleşiyorsa kabul edilebilir.
Yaygın kırmızı bayraklar:
ORDER BY ile birlikte)Yavaş planlar genellikle birkaç kalıp nedeniyle olur:
WHERE + ORDER BY deseninize uyan eksik indeks (ör. (user_id, status, created_at))WHERE içinde fonksiyon kullanımı (ör. WHERE lower(email) = $1), uygun bir expression index eklenmediyse taramalara zorlayabilirPlan garip görünüyorsa ve tahminler çok yanlışsa, istatistikler genelde güncel değildir. ANALYZE çalıştırın (veya autovacuum'un yakalamasına izin verin) ki Postgres güncel satır sayısını ve değer dağılımını öğrensin. Bu, büyük importlardan sonra veya yeni endpoint'ler hızlı veri yazmaya başlayınca önemlidir.
İndeksler yalnızca API'nizin veriyi sorguladığı şekilde çalıştığında yardımcı olur. Tahmin ederek indeks inşa ederseniz, yazma maliyeti artar, depolama büyür ve hızlanma çok az olur.
Pratik düşünce biçimi: bir indeks belirli bir soru için kestirmedir. API'niz farklı bir soru soruyorsa Postgres kestirmeyi görmezden gelir.
Bir endpoint account_id ile filtreliyor ve created_at DESC ile sıralıyorsa, tek bir bileşik indeks genelde iki ayrı indeksten daha iyidir. Postgres'e doğru satırları bulup doğru sırada döndürmek için daha az iş bırakır.
Genel kurallar:
Örnek: API'nizde GET /orders?status=paid varsa ve her zaman en yeniler gösteriliyorsa, (status, created_at DESC) gibi bir indeks iyi uyum sağlar. Eğer çoğu sorgu müşteri ile de filtreliyorsa (customer_id, status, created_at) daha iyi olabilir; ama sadece endpoint prod ortamında gerçekten böyle çalışıyorsa.
Trafiğin çoğu dar bir satır dilimine vuruyorsa, kısmi indeks daha ucuz ve hızlı olabilir. Örneğin uygulama çoğunlukla aktif kayıtları okuyorsa, sadece WHERE active = true ile indekslemek indeksi daha küçük tutar ve bellekte kalma ihtimalini artırır.
Bir indeksin işe yaradığını doğrulamak için hızlı kontroller:
EXPLAIN çalıştırın ve sorgunuza uygun bir index scan arayın.Kullanılmayan indeksleri dikkatle kaldırın. Kullanım istatistiklerine bakın (ör. bir indeks tarandı mı?). Düşürürken düşük riskli zamanları seçin ve geri alma planı hazırlayın. Kullanılmayan indeksler zararsız değildir; her yazmada insert/update yavaşlatırlar.
Sayfalama genellikle hızlı bir API'nin yavaş hissettirdiği noktadır, veritabanı sağlıklı olsa bile. Sayfalamayı UI detayı değil, sorgu tasarımı olarak ele alın.
LIMIT/OFFSET basit görünür, ama derin sayfalar daha pahalıdır. Postgres atlanan satırların üzerinden yine geçmek zorunda kalır (ve çoğu zaman sıralama yapar). 1. sayfa birkaç düzine satır okuyabilir. 500. sayfa ise 20 sonuç döndürmek için on binlerce satırı taramak ve atmak zorunda kalabilir.
Ayrıca istekler arasında satırlar eklenip silindiğinde sonuçları kararsız yapabilir. Kullanıcılar yinelemeler görebilir veya öğeleri kaçırabilir çünkü "10000. satır"ın anlamı değişmiştir.
Keyset sayfalama farklı bir soru sorar: "Bana son gördüğüm satırdan sonraki 20 satırı ver." Bu, veritabanının küçük, tutarlı bir dilim üzerinde çalışmasını sağlar.
Basit bir sürüm artan id kullanır:
SELECT id, created_at, title
FROM posts
WHERE id > $1
ORDER BY id
LIMIT 20;
API next_cursor olarak sayfadaki son id'yi döner. Sonraki istek bu değeri $1 olarak kullanır.
Zamana dayalı sıralama için kararlı bir sıra ve bağ çözücü kullanın. created_at tek başına yeterli değildir eğer iki satır aynı zaman damgasını paylaşıyorsa. Bileşik cursor kullanın:
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Tekrarlamalar ve eksik satırlar olmasını önleyecek birkaç kural:
ORDER BY'de her zaman benzersiz bir bağ çözücü ekleyin (genellikle id).created_at ve id'yi birlikte encode edin).Şaşırtıcı derecede yaygın bir neden API'nin yavaş hissetmesi veritabanı değildir. Yanıttır. Büyük JSON oluşturmak daha uzun sürer, göndermek daha uzun sürer ve istemcilerin parse etmesi daha uzun sürer. En hızlı kazanım genelde daha az döndürmektir.
SELECT ile başlayın. Bir endpoint sadece id, name ve status gerekiyorsa, sadece bu sütunları isteyin. SELECT * tabloya uzun metin, JSON blobları ve audit sütunları eklendikçe sessizce ağırlaşır.
Diğer sık yavaşlama sebebi N+1 yanıt oluşturma: 50 öğelik bir liste alırsınız, sonra ilişkili verileri eklemek için 50 ek sorgu çalıştırırsınız. Testleri geçebilir, gerçek trafik altında çökebilir. Tek sorguyla gerekeni döndürmeyi tercih edin (dikkatli join'ler), ya da ikinci sorguyu ID'lere göre batchleyin.
Yanıtları müşteriyi bozmadan küçük tutmanın birkaç yolu:
include= bayrağı (veya fields= maskesi) kullanın ki liste yanıtları hafif kalsın ve detay yanıtlar ekstra alanları isteyebilsin.Her ikisi de hızlı olabilir. Ne optimize ettiğinize bağlı olarak seçin.
Postgres JSON fonksiyonları (jsonb_build_object, json_agg) az tur ile ve tek sorguda öngörülebilir şekiller üretmek istediğinizde faydalıdır. Go'da şekillendirme ise koşullu mantık, struct yeniden kullanımı veya SQL'i daha okunabilir tutmak istediğinizde avantajlıdır. JSON oluşturma SQL'iniz okunması zorlaşırsa, onu optimize etmek de zorlaşır.
İyi bir kural: Postgres'e filtreleme, sıralama ve agregasyonu yaptırın. Sonrasında Go son sunumu halletsin.
Eğer API'leri hızlı üretiyorsanız (ör. Koder.ai ile), include bayraklarını erken eklemek endpoint'lerin zaman içinde şişmesini önlemeye yardımcı olur. Ayrıca her yanıtı ağırlaştırmadan alan eklemenin güvenli bir yolunu verir.
Büyük bir test laboratuvarına ihtiyacınız yok; kısa, tekrar edilebilir bir geçiş çoğu performans sorununu ortaya çıkarır—özellikle başlangıç noktası üretim için hazırlanmış ve jenerik kod olduğunda.
Herhangi bir şeyi değiştirmeden önce küçük bir temel yazın:
Küçük başlayın, aynı anda bir şey değiştirin ve her değişiklikten sonra tekrar test edin.
Gerçeğe yakın bir 10–15 dakikalık yük testi çalıştırın. İlk kullanıcıların vuracağı aynı endpoint'lere (login, listeleme, arama, oluşturma) istek atın. Sonra route'ları p95 gecikmeye ve toplam geçire zamanına göre sırala.
SQL'i tune etmeden önce bağlantı basıncı olup olmadığına bakın. Çok büyük bir havuz Postgres'i boğar. Çok küçük bir havuz uzun beklemeler yaratır. Bekleme süresinde artış ve bağlantı sayılarında ani sıçramalara bakın. İlk önce havuz ve idle limitlerini ayarlayın, sonra aynı yük testi tekrar çalıştırın.
En yavaş sorgulara EXPLAIN uygulayın ve en büyük kırmızı bayrağı düzeltin. Yaygın suçlular: büyük tablolarda tam tablo taramaları, büyük sonuç kümelerinde sıralamalar ve patlayan join'ler. Tek en kötü sorguyu seçin ve onu sıkıcı hale getirin.
Bir indeks ekleyin veya ayarlayın, sonra tekrar test edin. İndeksler WHERE ve ORDER BY'ınıza uyduğunda yardımcı olur. Bir seferde beş tane eklemeyin. Eğer yavaş endpoint "kullanıcıya göre siparişleri created_at'a göre listele" ise (user_id, created_at) bileşik indeksi anlık ile acı arasında fark yaratabilir.
Yanıtları ve sayfalamayı sıkıştırın, sonra tekrar test edin. Eğer endpoint büyük JSON bloblarıyla 50 satır döndürüyorsa veritabanı, ağ ve istemci hepsi bedel öder. UI'nin ihtiyaç duyduğu alanları döndürün ve tablolar büyüdükçe yavaşlamayan sayfalama tercih edin.
Basit bir değişiklik günlüğü tutun: ne değişti, neden ve p95'te ne hareket etti. Bir değişiklik temelinizi iyileştirmiyorsa geri alın ve devam edin.
Çoğu performans problemi Go API'lerde Postgres ile kendiliğinden yapılır. İyi haber: birkaç kontrol gerçek trafik gelmeden çoğunu yakalar.
Klasik bir tuzak havuz boyutunu "olabildiğince büyük" yapmak. Bu genelde her şeyi yavaşlatır. Postgres oturumları, bellek ve kilitlerle daha fazla vakit geçirir ve uygulamanız dalga dalga zaman aşımına girer. Küçük, stabil bir havuz genelde kazanır.
Bir diğer hata "her şeyi indeksle"mektir. Fazla indeksler okuma performansına yardımcı olabilir ama yazmaları yavaşlatır ve sorgu planlarını şaşırtıcı şekilde değiştirebilir. Ölçün, sonra ekleyin; indeks ekledikten sonra planları tekrar kontrol edin.
Sayfalama borcu sessizce girer. Offset sayfalama başta iyi görünür, sonra p95 zamanla yükselir çünkü veritabanı daha fazla satırın üzerinden geçmek zorunda kalır.
JSON yükü gizli bir vergi gibidir. Sıkıştırma bant genişliğini azaltır ama büyük nesneleri oluşturma, tahsis etme ve parse etme maliyetini ortadan kaldırmaz. Alanları kırpın, derin iç içe yapıları azaltın ve ekranda gerçekten gerekli olmayanları döndürmeyin.
Sadece ortalama yanıt süresine bakıyorsanız, gerçek kullanıcı acısını kaçırırsınız. p95 (ve bazen p99) havuz dolumu, kilit beklemeleri ve yavaş planların ilk göründüğü yerdir.
Kısa bir lansman öncesi kontrolü:
EXPLAIN'ı yeniden çalıştırın.Gerçek kullanıcılar gelmeden önce API'nizin stres altında öngörülebilir kaldığına dair kanıt istiyorsunuz. Amaç mükemmel sayılar değil—zaman aşımına, zirvelere veya veritabanının yeni iş kabul etmeyi durdurmasına yol açacak birkaç sorunu yakalamaktır.
Staging benzeri bir ortamda (yakın DB boyutu, aynı indeksler, aynı havuz ayarları) kontrolleri çalıştırın: önemli endpoint'ler için yük altında p95 ölçün, toplam sürede en yavaş sorguları yakalayın, havuz bekleme süresini izleyin, en kötü sorguya EXPLAIN (ANALYZE, BUFFERS) uygulayıp beklediğiniz indeksi kullandığını doğrulayın ve en yoğun rotalarınızdaki payload boyutlarını mantık kontrolünden geçirin.
Sonra ürünlerin kırılma biçimini taklit eden bir en kötü durum çalıştırması yapın: derin bir sayfa isteği, en geniş filtreyi uygulama ve soğuk başlangıçla deneme (API'yi yeniden başlatıp aynı isteği ilk olarak atma). Eğer derin sayfalama sayfa sayısı arttıkça yavaşlıyorsa, lansmandan önce cursor tabanlı sayfalamaya geçin.
Varsayılanlarınızı yazın ki ekip daha sonra tutarlı seçimler yapsın: havuz limitleri ve time-out'lar, sayfalama kuralları (maks sayfa boyutu, offset izinli mi, cursor formatı), sorgu kuralları (sadece gerekli sütunları seç, SELECT *'ten kaçın, pahalı filtreleri sınırla) ve logging kuralları (slow query eşik değeri, örnekleri ne kadar saklayacağınız, endpoint etiketleme).
Eğer Go + Postgres servislerini Koder.ai ile oluşturup dışa aktarıyorsanız, dağıtımdan önce kısa bir planlama aşaması filtreleri, sayfalamayı ve yanıt şekillerini kasıtlı kılmada yardımcı olur. İndeksleri ve sorgu şekillerini iyileştirmeye başlayınca, anlık görüntüler (snapshots) ve rollback bir değişikliğin bir endpoint'e iyi gelip diğerlerine zarar veriyorsa geri almayı kolaylaştırır. Tek bir yerde bu iş akışını yinelemek isterseniz, Koder.ai koder.ai üzerinde sohbet etrafında bu servisleri üretip rafine etmek ve hazır olduğunuzda kaynak kodu dışa aktarmak üzere tasarlanmıştır.
Start by separating DB wait time from app work time.
Add simple timing around “wait for connection” and “query execution” to see which side dominates.
Use a small baseline you can repeat:
Pick a clear target like “p95 under 200 ms at 50 concurrent users, errors under 0.5%.” Then only change one thing at a time and re-test the same request mix.
Enable slow query logging with a low threshold in pre-launch testing (for example 100–200 ms) and log the full statement so you can copy it into a SQL client.
Keep it temporary:
Once you’ve found the worst offenders, switch to sampling or raise the threshold.
A practical default is a small multiple of CPU cores per API instance, often 5–20 max open connections, with similar max idle connections, and recycle connections every 30–60 minutes.
Two common failure modes:
Remember pools multiply across instances (20 connections × 10 instances = 200 connections).
Time DB calls in two parts:
If most time is pool wait, adjust pool sizing, timeouts, and instance counts. If most time is query execution, focus on EXPLAIN and indexes.
Also confirm you always close rows promptly so connections return to the pool.
Run EXPLAIN (ANALYZE, BUFFERS) on the exact SQL your API sends and look for:
ORDER BY)Fix the biggest red flag first; don’t tune everything at once.
Indexes should match what the endpoint actually does: filters + sort order.
Good default approach:
WHERE + ORDER BY pattern.Example: if you filter by user_id and sort by newest, an index like (user_id, created_at DESC) is often the difference between stable p95 and spikes.
Use a partial index when most traffic hits a predictable subset of rows.
Example pattern:
active = trueA partial index like ... WHERE active = true stays smaller, is more likely to fit in memory, and reduces write overhead versus indexing everything.
Confirm with EXPLAIN that Postgres actually uses it for your high-traffic queries.
LIMIT/OFFSET gets slower on deep pages because Postgres still has to walk past (and often sort) the skipped rows. Page 500 can be dramatically more expensive than page 1.
Prefer keyset (cursor) pagination:
id).ORDER BY identical across requests.(created_at, id) or similar into a cursor.This keeps each page cost roughly constant as tables grow.
Usually yes for list endpoints. The fastest response is the one you don’t send.
Practical wins:
SELECT *).include= or fields= so clients opt into heavy fields.You’ll often reduce Go CPU, memory pressure, and tail latency just by shrinking payloads.