Ana içeriğe geç
Version: 1.0.1

SQL Performance Tuning

Bu makale, Ignite SQL sorguları için temel ve gelişmiş optimizasyon tekniklerini özetlemektedir. Bazı bölümler hata ayıklama ve sorun giderme için de kullanışlıdır.

Basic Considerations: Ignite vs RDBMS

Ignite, bir RDBMS için oluşturulan mevcut SQL sorgularının direkt olarak çalışacağı ve herhangi bir değişiklik olmadan Ignite'ta daha hızlı performans göstereceği beklentisiyle SQL yetenekleri açısından sıklıkla ilişkisel veritabanlarıyla karşılaştırılır. Genellikle böyle bir varsayım, Ignite'ın verileri bellekte depolaması ve işlemesi gerçeğine dayanır. Ancak, verileri yalnızca RAM'e koymak ve performansta önemli derecede artış beklemek için yeterli değildir. Genel olarak, ekstra ayar gereklidir. Aşağıda, Ignite'ı bir RDBMS ile kıyaslamadan veya herhangi bir performans testi yapmadan önce dikkate alınması gereken best practice’lerin standart bir listesini görebilirsiniz:

  • Ignite, birincil depolama olarak RAM ile çok node’lu dağıtımlar için optimize edilmiştir. Tek node’lu bir Ignite cluster’ını ilişkisel bir veritabanıyla karşılaştırmaya çalışmayın. Verilerin tüm kopyası RAM'de olacak şekilde çok node’lu bir Ignite cluster’ı deploy etmelisiniz.
  • Veri modelinizi ve mevcut SQL sorgularınızı ayarlamaya hazır olun. Uygun veri dağıtımı için veri modelleme aşamasında affinity colocation konseptini kullanın. Unutmayın, sadece RAM'e veri koymak yeterli değildir. Verileriniz düzgün bir şekilde bir arada konumlandırılırsa, JOIN'lerle büyük ölçekte SQL sorguları çalıştırabilir ve önemli performans avantajları bekleyebilirsiniz.
  • İkincil indexleri tanımlayın ve aşağıda açıklanan diğer standart ve Ignite'a özgü ayarlama tekniklerini kullanın.
  • İlişkisel veritabanlarının local önbelleğe alma tekniklerinden yararlandığını ve toplam veri boyutuna bağlı olarak bir RDBMS'nin çok node’lu bir yapılandırmada bile bazı sorguları Ignite'tan bile daha hızlı tamamlayabileceğini unutmayın. Veri kümeniz yaklaşık 10-100 GB ise ve bir RDBMS, verileri local olarak önbelleğe almak için yeterli RAM'e sahipse, çok düğümlü bir Ignite kümesinden daha iyi performans gösterebilir çünkü ikincisi network kullanacaktır. Farkı görmek için Ignite'ta çok daha fazla veri depolayın.

Using the EXPLAIN Statement

Ignite, bir sorgunun yürütme planını okumak için kullanılabilecek EXPLAIN ifadesini destekler. Olası optimizasyon için sorgularınızı analiz etmek üzere bu komutu kullanın. Planın birden çok satır içereceğini unutmayın: sonuncusu, indirgeme tarafı (genellikle sizin uygulamanız) için bir sorgu içerecektir, diğerleri map nodeları içindir (genellikle server nodeları). Ignite'ta sorguların nasıl yürütüldüğünü öğrenmek için Distributed Queries bölümünü okuyun.

EXPLAIN SELECT name FROM Person WHERE age = 26;

Yürütme planı, burada açıklandığı gibi H2 tarafından oluşturulur.

OR Operator and Selectivity

Bir sorgu OR işleci içeriyorsa, sorgunun karmaşıklığına bağlı olarak indexler beklendiği gibi kullanılmayabilir. Örneğin, select name from Person where gender='M' and (age = 20 or age = 30), sorgusu için age alanındaki bir index yerine gender alanındaki bir index kullanılacaktır, ancak ikincisi bir daha seçici indekstir. Bu soruna geçici bir çözüm olarak, sorguyu UNION ALL ile yeniden yazabilirsiniz (ALL olmadan UNION'ın DISTINCT satırları döndüreceğine dikkat edin, bu da sorgu semantiğini değiştirecek ve sorgu performansınızı daha da azaltacaktır):

SELECT name FROM Person WHERE gender='M' and age = 20
UNION ALL
SELECT name FROM Person WHERE gender='M' and age = 30

Avoid Having Too Many Columns

Bir SELECT sorgusunun sonuç kümesinde çok fazla sütun bulundurmaktan kaçının. H2 sorgu ayrıştırıcısının sınırlamaları nedeniyle, 100'den fazla sütuna sahip sorgular beklenenden daha kötü performans gösterebilir.

Lazy Loading

Varsayılan olarak Ignite, tüm sonuç kümesini belleğe yüklemeye ve sorgu başlatıcıya (genellikle uygulamanızdır) geri göndermeye çalışır. Bu yaklaşım, küçük veya orta sonuç kümelerinin sorguları için optimum performans sağlar. Ancak, sonuç kümesi kullanılabilir belleğe sığmayacak kadar büyükse, uzun süreli GC duraklamalarına ve hatta OutOfMemoryError hatalarına yol açabilir.

Bellek tüketimini en aza indirmek için, orta düzeyde bir performans düşüşü pahasına, lazy parametresini JDBC ve ODBC bağlantı connection stringlerine yazarak sonuç kümelerini lazy bir şekilde yükleyebilir ve işleyebilirsiniz veya Java, .NET ve C++ API'leri için mevcut olan benzer bir yöntemi kullanabilirsiniz;

var query = new SqlFieldsQuery("SELECT * FROM Person WHERE id > 10")
{
// Result set will be loaded lazily.
Lazy = true
};

Querying Colocated Data

Ignite dağıtık bir sorgu yürüttüğünde, verileri getirmek için ayrı ayrı cluster nodelarına alt sorgular gönderir ve sonuçları reducer node’da(genellikle sizin uygulamanız) gruplandırır. Sorguladığınız verilerin GROUP BY koşulu tarafından aynı yere yerleştirildiğini önceden biliyorsanız, SQL altyapısına remote nodelarda gruplandırma yapmasını söylemek için SqlFieldsQuery.collocated = true kullanabilirsiniz. Bu, nodelar arasındaki ağ trafiğini ve sorgu yürütme süresini azaltacaktır. Bu flag true olarak ayarlandığında, sorgu önce tek tek nodelarda yürütülür ve sonuçlar son hesaplama için reducer node’a gönderilir.

Verilerin department_id tarafından colocated olarak konumlandırıldığını varsaydığımız aşağıdaki örneği ele alalım (başka bir deyişle, department_id alanı affinity key olarak yapılandırılmıştır).

SELECT SUM(salary) FROM Employee GROUP BY department_id

SUM işleminin doğası gereği, Ignite, node’larda depolanan öğeler arasındaki salary’leri toplayacak ve ardından bu toplamları, nihai sonucun hesaplanacağı reducer node’a gönderecektir. Bu işlem zaten dağıtıktır ve collocated flag’in etkinleştirilmesi performansı yalnızca biraz artıracaktır.

Biraz farklı bir örnek verelim:

SELECT AVG(salary) FROM Employee GROUP BY department_id

Bu örnekte, Ignite'ın tüm (salary, department_id) çiftlerini reducer node’a getirmesi ve sonuçları orada hesaplaması gerekir. Bununla birlikte, çalışanlar department_id alanı tarafından aynı yere yerleştirildiyse, yani aynı departman için çalışan verileri aynı düğümde depolanıyorsa, SqlFieldsQuery.collocated = true ayarı sorgu yürütme süresini kısaltacaktır, çünkü Ignite bağımsız düğümlerdeki her departman için ortalamaları hesaplar ve son hesaplama için sonuçları reducer node’a gönderir.

Query Parallelism

Varsayılan olarak, her Ignite node’unda tek bir threadde bir SQL sorgusu yürütülür. Bu yaklaşım, index aramayı içeren küçük sonuç kümeleri döndüren sorgular için idealdir. Örneğin:

SELECT * FROM Person WHERE p.id = ?;

Belirli sorgular, birden çok threadde yürütülmekten fayda sağlayabilir. Bu, genellikle HTAP ve OLAP iş yükleri için geçerli olan tablo taramaları ve toplamaları içeren sorgularla ilgilidir. Örneğin:

SELECT SUM(salary) FROM Person;

Sorgu yürütme için tek bir nodeda oluşturulan thread sayısı cache başına yapılandırılır ve varsayılan olarak 1'e eşittir. CacheConfiguration.queryParallelism parametresini ayarlayarak değeri değiştirebilirsiniz. CREATE TABLE komutunu kullanarak SQL tabloları oluşturursanız, bu parametreyi ayarlamak için bir cache şablonu kullanabilirsiniz.

Bir sorgu JOIN'ler içeriyorsa, katılan tüm cacheler aynı derecede paralelliğe sahip olmalıdır.

Index Hints

Index ipuçları, bir indexin belirli sorgular için diğerinden daha uygun olduğunu bildiğiniz senaryolarda kullanışlıdır. Bunları, query optimizer’a daha verimli bir yürütme planı seçmesi talimatını vermek için kullanabilirsiniz. Bunun için aşağıdaki örnekte gösterildiği gibi USE INDEX(indexA,…,indexN) deyimini kullanabilirsiniz.

SELECT * FROM Person USE INDEX(index_age)
WHERE salary > 150000 AND age < 35;

Partition Pruning

Partition pruning, WHERE koşulunda affinity keylerini kullanan sorguları optimize eden bir tekniktir. Böyle bir sorgu yürütülürken, Ignite yalnızca istenen verilerin depolandığı partitionaları tarar. Sorgu yalnızca istenen partitionaları depolayan nodelara gönderileceği için bu, sorgu süresini azaltacaktır.

Aşağıdaki örnekte, employee nesneleri id alanı tarafından colocated’tır(bir affinity key doğrudan ayarlanmamışsa, affinity key olarak primary key kullanılır):

CREATE TABLE employee (id BIGINT PRIMARY KEY, department_id INT, name VARCHAR)

/* This query is sent to the node where the requested key is stored */
SELECT * FROM employee WHERE id=10;

/* This query is sent to all nodes */
SELECT * FROM employee WHERE department_id=10;

Bir sonraki örnekte affinity key doğrudan ayarlanmıştır ve bu nedenle verileri bir colocated hale getirmek ve sorguları verilerin birincil kopyalarını tutan nodelara yönlendirmek için kullanılacaktır:

CREATE TABLE employee (id BIGINT PRIMARY KEY, department_id INT, name VARCHAR) WITH "AFFINITY_KEY=department_id"

/* This query is sent to all nodes */
SELECT * FROM employee WHERE id=10;

/* This query is sent to the node where the requested key is stored */
SELECT * FROM employee WHERE department_id=10;

Skip Reducer on Update

Ignite bir DML işlemi yürüttüğünde, önce etkilenen tüm ara satırları analiz için reducer node’a (genellikle uygulamanız) getirir ve ancak bundan sonra remote nodelara gönderilecek güncellenmiş değerlerin batchlerini hazırlar.

Bir DML işleminin birçok girişi taşıması gerekiyorsa, bu yaklaşım performansı etkileyebilir ve ağı sature edebilir.

SQL motorunun server nodelarında tüm ara satır analizlerini ve güncellemelerini "yerinde" yapması için bu flagi bir ipucu olarak kullanın. İpucu, JDBC ve ODBC bağlantıları için desteklenir.

//jdbc connection string
jdbc:ignite:thin://192.168.0.15/skipReducerOnUpdate=true

SQL On-heap Row Cache

Ignite, verileri ve indexleri Java heap dışında kendi bellek alanında depolar. Bu, her veri erişiminde, verilerin bir kısmının off-heap alandan Java heap’e kopyalanacağı, potansiyel olarak deserialize olacağı ve uygulamanız veya server node’unuz ona başvurduğu sürece heapte tutulacağı anlamına gelir.

Heap üzerinde SQL row cache, etkin satırları (key-value nesneleri) Java heapte depolamayı ve veri kopyalama ve deserialization için harcanan kaynakları en aza indirmeyi amaçlar. Cache’e alınan her satır, off-heap bölgedeki bir girişi ifade eder ve aşağıdakilerden biri gerçekleştiğinde geçersiz kılınabilir:

  • Off-heap bölgede saklanan ana giriş güncellenir veya kaldırılır.
  • Ana girişi saklayan veri sayfası RAM'den çıkarılır.

On-heap row cache, belirli bir cache/tablo için etkinleştirilebilir (SQL tabloları ve cacheleri oluşturmak için CREATE TABLE kullanıyorsanız, parametre bir cache şablonu aracılığıyla iletilebilir):

<bean class="org.apache.ignite.configuration.IgniteConfiguration">
<property name="cacheConfiguration">
<bean class="org.apache.ignite.configuration.CacheConfiguration">
<property name="name" value="myCache"/>
<property name="sqlOnheapCacheEnabled" value="true"/>
</bean>
</property>
</bean>

Row cache etkinleştirildiyse, performans için RAM ticareti yapabilirsiniz. Satırları cache’e alma amacıyla daha fazla RAM ayırarak bazı SQL sorguları ve kullanım durumları için 2 kata kadar performans artışı elde edebilirsiniz.

Şu anda, cache sınırsızdır ve bellek veri bölgelerinize tahsis edilen kadar RAM kaplayabilir. Şunlardan emin olun;

  • JVM maksimum heap boyutunu, bu on-heap row cache’inin etkinleştirildiği cacheleri depolayan tüm veri bölgelerinin toplam boyutuna eşit olarak ayarlayın.
  • JVM garbage collection işlemini buna göre ayarlayın.

Using TIMESTAMP instead of DATE

Mümkün olduğunda DATE yerine TIMESTAMP türünü kullanın. Şu anda, DATE türü çok verimsiz bir şekilde serialize/deserialize olur, bu da performans düşüşüne neden olur.