Aşağıdaki liste bazı yararlı bilgiler içermektedir ve veritabanına dayalı Go uygulaması ile çalışırken çok faydalı olabilir, bu nedenle önerileri kendi kodunuzda test etmek ve uygulamak size kalmıştır. Bilgilerin çoğu Go database/sql tutorial ve The Ultimate Guide To Building Database - Driven Apps with Go dökümanlarından derlenmiştir.


Burada öğreneceğiniz ve dikkat etmeniz gereken en önemli şey db.Prepare fonksiyonunun kullanılmasıdır. Önerileri takip etmezseniz, fonksiyonunun gerçekten ne yaptığını ve nasıl yaptığını anlamazsanız, bu durum gerçekten sizin ve uygulamanız için biraz can sıkıcı olabilir! Aşağıda yazılan bilgilerin haricinde, yukarıda belirtilen ikinci linkteki dökümanı okumanızı şiddetle tavsiye ederim çünkü, konular hakkında daha ayrıntılı bilgiler bulabilirsiniz.


1) Sorgularda prepare kullanma veya kullanmama


SQL sorgularınızın (insert, update, delete vs.) bir parçası olarak kullandığınız verilerin temiz olduğundan %100 eminseniz (SQL enjeksiyonsuz), db.Prepare yerine db.Exec kullanmayı tercih etmelisiniz. Bunun nedeni ise, sorgunuz içinde argümanlar için bir yer tutucusu olsada olmasada, tek bir sorguyu db.Prepare ile çalıştırmak isterseniz toplam üç ekstra sorgu (Prepare, Execute ve Close) kullanılacaktır ki, bu sistem kaynaklarını ziyan etmek anlamına gelir. Eğer db.Exec kullanırsanız toplam bir sorgu (Query) kullanılacaktır. Aşağıdaki örnek, tabloya tek bir kayıt eklemek içindir. Gördüğümüz gibi db.Prepare ağ üzerinde üç tur atarken, db.Exec sadece bir tur atıyor. Not: Eğer parametreleri kendiniz temizleyip kullanmak isterseniz fmt.Sprintf() fonksiyonunu %q yer tutucusu ile kullanabilirsiniz - örnek: fmt.Sprintf(`INSERT INTO table (name, age, active) VALUES (%q, %d, %t)`)


db.Prepare


func Insert() (int64, error) {
stmt, err := db.Prepare("INSERT INTO users (name) VALUES (?)")
if err != nil {
return 0, err
}
defer stmt.Close()

res, err := stmt.Exec("inanzzz")
if err != nil {
return 0, err
}

id, err := res.LastInsertId()
if err != nil {
return 0, err
}

return id, nil
}

2019-12-28T21:12:53.517599Z	    3 Prepare	INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz')
2019-12-28T21:12:53.527352Z 3 Close stmt

db.Exec


func Insert() (int64, error) {
res, err := db.Exec("INSERT INTO user (name) VALUES ('inanzzz')")
if err != nil {
return 0, err
}

id, err := res.LastInsertId()
if err != nil {
return 0, err
}

return id, nil
}

2019-12-28T21:13:11.337554Z	    3 Query	INSERT INTO user (name) VALUES ('inanzzz')

Karşılaştırma


100 kullanıcı, 10 saniye ara vermeden aynı anda istek gönderiyor. Aşağıdaki bulgular sadece kaba hesaplamalardır.


// db.Prepare
Average total requests: 4500
Average request duration: 210ms

// db.Exec
Average total requests: 8500
Average request duration: 120ms

2) Döngünün içinde veya dışında prepare kullanımı


Eğer aynı sorguyu birden fazla değişik parametre ile kullanacaksanız, stmt.Exec fonksiyonu döngünün içinde tutulurken, db.Prepare fonksiyonun mutlaka döngünün dışında tutulması gereklidir. Bunun nedeni ise, döngü içinde aynı sorgunun sürekli hazırlanıp ve ardından kapatılması sistem kaynaklarının ziyanından başka hiçbir anlam ifade etmez. Ayrıca, döngü içinde defer stmt.Close() kullanılması kaynak sızıntısına neden olacağı için, gerçekleşmesi muhtemel olan bir felaketi beklemeye benzer. Sonuç olarak, döngü içinde db.Prepare kullanımı, dışarıda kullanımına nazaran sorgu miktarını iki katına çıkarır. Aşağıdaki örneklere bakın.


İçinde


func Insert() error {
for i := 1; i < 4; i++ {
stmt, err := r.database.Prepare(`INSERT INTO users (name) VALUES (?)`)
if err != nil {
return err
}
defer stmt.Close()

_, err := stmt.Exec(fmt.Sprintf("inanzzz-%d", i))
if err != nil {
return err
}
}

return nil
}

Gördüğümüz gibi toplam olarak 9 sorgu çalıştırıldı.


2019-12-28T21:12:53.517599Z	    3 Prepare	INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-1')
2019-12-28T21:12:54.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:54.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-2')
2019-12-28T21:12:55.517599Z 3 Prepare INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:55.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-3')
2019-12-28T21:12:56.527352Z 3 Close stmt
2019-12-28T21:12:56.527352Z 3 Close stmt
2019-12-28T21:12:56.527352Z 3 Close stmt

Dışında


func Insert() error {
stmt, err := db.Prepare(`INSERT INTO users (name) VALUES (?)`)
if err != nil {
return err
}
defer stmt.Close()

for i := 1; i < 4; i++ {
_, err := stmt.Exec(fmt.Sprintf("inanzzz-%d", i))
if err != nil {
return err
}
}

return nil
}

Gördüğümüz gibi toplam olarak 5 sorgu çalıştırıldı.


2019-12-28T21:12:53.517599Z	    3 Prepare	INSERT INTO user (name) VALUES (?)
2019-12-28T21:12:53.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-1')
2019-12-28T21:12:54.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-2')
2019-12-28T21:12:55.518932Z 3 Execute INSERT INTO user (name) VALUES ('inanzzz-3')
2019-12-28T21:12:55.527352Z 3 Close stmt

3) LastInsertId ve RowsAffected


result.LastInsertId() ve result.RowsAffected() fonksiyonlarını sadece son eklenen kaydın ne olduğunu veya kaç kaydın eklendiğini açıkça bilmeniz gerekiyorsa kullanın. Aksi takdirde bunları varsayılan olarak kullanmayın çünkü, bu işlemler veritabanına erişmeseler bile, ilişkide oldukları veritabanı bağlantıları o an için meşgulse, bu durum "bekletme" işlemi haline gelebilir. Ayrıca, tüm veritabanı sürücüleri de bu özellikleri desteklemez.


4) Query/QueryRow ve Exec


Eğer sorgunuz veri çekmek için değilse, db.Query ve db.QueryRow yerine db.Exec kullanmanız daha mantıklı olacaktır. Bunun ana nedeni, db.Exec bağlantısını havuza anında geri bırakırken, db.Query bağlantısını rows.Close() çağrılıncaya kadar havuzun dışında tutar. Eğer bunu yapmazsanız, bağlantı "sızıntısına" ve sunucunun kullanılabilir bağlantılarının tükenmesine neden olabilirsiniz.


Eğer select sorgusu kullanıyorsanız ve de sorgunuz içinde argümanlar için bir yer tutucusu olsada olmasada, db.Prepare & stmt.Exec kombinasyonun yerine, db.Query ve db.QueryRow kullanmaya devam etmelisiniz. Bunun nedeni ise, sorgunuz içinde argümanlar için bir yer tutucusu yok ise, db.Query ve db.QueryRow otomatik olarak db.Exec gibi çalışarak ve sadece bir sorgu (Query) kullanılacaktır ki bu da istediğimiz senaryodur.


5) db.SetMaxIdleConns


Bu ayar serbest bırakıldıktan sonra havuzda boşta kalması gereken bağlantıların miktarını tanımlar. Veritabanında SHOW PROCESSLIST; sorgusunu kullanarak doğrulama yapabilirsiniz. Varsayılan değer 2'dir. Bu varsayılan değer, çok sayıda bağlantının birbirleri ardına kapatılmasına ve açılmasına neden olur ki, bu da kaçınmak istediğiniz bir şeydir. Ekstra çalışma ve gecikme süresinin yanı sıra, yeni bir bağlantı beklerken gecikmeye yol açar. Güvenli bir şekilde, bunun yerine 25 veya 5 (ideal) gibi daha yüksek bir değere ayarlayabilirsiniz.


Karşılaştırma


100 kullanıcı aynı anda 10 saniye ara vermeden istek gönderiyor. Aşağıdaki bulgular sadece kaba hesaplamalardır.


// db.SetMaxIdleConns = 2 (default)
Average total requests: 3800
Average request duration: 250ms

// db.SetMaxIdleConns = 5
Average total requests: 4700
Average request duration: 200ms

// db.SetMaxIdleConns = 25
Average total requests: 5500
Average request duration: 180ms

6) db.SetMaxOpenConns


Veritabanındaki mevcut açık bağlantı sayısını tanımlar. Varsayılan değer sınırsızdır. Varsayılan değerini değiştirmek istiyorsanız dikkatli olmalısınız. Herhangi bir sorgu içeriği context.WithTimeout gibi bir zaman aşımı özelliği uyguluyorsa, en ufak bir değişiklik potansiyel olarak context deadline exceeded hatalarına yol açabilir. Bekleyen sorguları işlemek için yeterli açık bağlantı olmayabilir, bu nedenle birçok sorgu zaman aşımına uğrar. Ayrıca performans düşecektir. %100 emin olmadığınız sürece bunu değiştirmekten kaçınma daha iyidir.


7) db.SetConnMaxLifetime


Bir bağlantının maksimum kullanılabilirlik süresini tanımlar. Varsayılan değer sınırsızdır. İdeal zaman ayrıca db.SetMaxIdleConns değerine de bağlıdır yani, db.SetMaxIdleConns yükselince db.SetConnMaxLifetime düşer. Kaba örnek: db.SetMaxIdleConns: 5 - db.SetConnMaxLifetime: 30 minutes, db.SetMaxIdleConns: 10 - db.SetConnMaxLifetime: 15 minutes. Çok yoğun bir veritabanına dayalı uygulamanız varsa, sayıları daha yüksek tutabilirsiniz. Uzun ömürlü birçok boş bağlantı kullanmak, yüksek sistem kaynakları gerektirir.


Ek bilgiler


Döngü içinde erteleme


Bir döngü içinde defer rows.Close() satırlarını çağırmayın. Potansiyel olarak bellek veya bağlantı "sızıntısına" neden olacaktır.


Birçok db nesnesini açma


Uygulama önyüklemesinde sql.DB örneğini yalnızca bir kez oluşturun ve tüm isteklerin bunu kullanmasına izin verin. Aksi takdirde uygulama veritabanına birçok TCP bağlantısını açıp kapatacak ve muhtemelen sunucuyu kilitleyecektir.


Satırları kapatmayı unutmak


Ertelemeden rows.Close() fonksiyonunu çalıştırın, aksi takdirde bağlantı "sızıntısına" neden olabilirsiniz.


Prepare'den kaçının


Elinizden geldiği sürece performansı artırmak için db.Prepare kullanmayarak daha az sorgu çalıştırabilir ve aynı sorgular için gereksiz ağ gidiş-dönüş gezilerini sonlandırabilirsiniz. fmt.Sprintf() fonksiyonunu %q yer tutucusu ile kullanarak sorgu parametrelerini kendiniz temizleyip kullanabirsiniz.


Seçimsiz sorgular için query kullanma


Eğer Select sorgusu kullanmıyorsanız db.Query ve db.QueryRow kullanmayın. Onun yerine db.Exec kullanın.


Null alanlarla çalışma


Bir tablodaki alan boş değer döndürüyorsa, struct veya değişken türünüz de boş değer olarak ayarlanmalıdır. Örnek: sql.NullString, mysql.NullTime vs.


Uint64 parametreleriyle çalışma


Süprizlerden hoşlanmıyorsanız fmt.Sprint() kullanarak uint64 parametreleri öncelikle string verilerine çevirin çünkü, db.Query, db.QueryRow ve db.Exec fonksiyonları uint64 parametreleriyle zaman zaman sorun yaşayabiliyorlar