SQLite İndeks Görselleştirmesi
(mrsuh.com)- SQLite indekslerinin gerçekte disk ve bellekte nasıl yerleştiğini görmek için B-Tree yapısı analiz edildi; indeks verileri dökülüp görselleştirildi
- İndeksler Page ve Cell birimlerinden oluşur; Page sağ çocuk bağlantısını ve Cell verilerini, Cell ise indeks verisi·rowId·sol çocuk bağlantısını taşır
sqlite3_analyzertarafından sağlanan Page boyutu, girdi sayısı, B-tree derinliği ve kullanılan Page sayısı tek başına yeterli olmadığından SQLite kaynak koduna debug fonksiyonları eklendi- Deneylerde kayıt sayısı, ASC/DESC, ifade tabanlı indeks, NULL içeren UNIQUE, Partial Index, çoklu sütun, metin·REAL·tamsayı+metin kombinasyonları karşılaştırıldı
- 1.000.000 kayıtta indeks eklemeden önce oluşturulunca 3.342 Pages, eklemeden sonra oluşturulunca 2.930 Pages oldu; VACUUM veya REINDEX sonrasında da 2.930 Pages’a düştü
SQLite indeksini doğrudan inceleme nedeni
- Bu, indeksin temel yapısının ötesine geçip gerçek veri yapısını, algoritmayı ve diskte saklanma biçimini doğrulamaya yönelik bir deneydir
- Amaç, DBMS’in indeksi disk ve bellekte nasıl sakladığını ve arama sürecinde buna nasıl eriştiğini incelemektir
- Deney hedefi olarak SQLite seçilmesinin nedenleri şunlardır
- Tarayıcılarda, mobil uygulamalarda ve işletim sistemlerinde yaygın kullanılan bir DBMS’tir
- Ayrı bir sunucu olmadan yalnızca istemci uygulamasıyla debug etmek kolaydır
- MySQL veya PostgreSQL’e göre kod tabanı daha küçüktür, ancak indekslerde benzer veri yapıları kullanır
- Açık kaynaktır
Page ve Cell’den oluşan B-Tree
- SQLite dokümantasyonuna göre indeksler B-Tree yapısı olarak saklanır
- SQLite’ta Node’a karşılık gelen birim Page’dir
- Page, Cell verilerini saklar
- Page, sağ çocuk Page’e giden bir bağlantıya sahiptir
- Cell, indeks verisini, rowId’yi ve sol çocuk Page bağlantısını içerir
- SQLite tablolarındaki her satır varsayılan olarak benzersiz bir rowId’ye sahiptir ve açık bir birincil anahtar olmadığında birincil anahtar gibi davranır
- Her Page sabit bir boyuta sahiptir; boyut aralığı 512~65.536 bytes’tır
- Page ve Cell başlıkları, çocuk bağlantısını saklamak için 4 bytes kullanır
- Çocuk Page numarasını öğrenmek için başlığı
get4byte(...)fonksiyonuyla ayrıca okumak gerekir
- Çocuk Page numarasını öğrenmek için başlığı
- SQLite iç yapılarına örnekler şöyledir
MemPage: Page numarasıpgno, Cell sayısınCell, Cell indeks alanıaCellIdx, Page verisinin disk imajı işaretçisiaDatavb. içerirCellInfo: payload başlangıç konumunu gösterenpPayloadvb. içerir
sqlite3_analyzer’ın sınırları ve debug fonksiyonları
- sqlite3_analyzer ile indeksin genel bilgileri görülebilir
- Örnek çıktıda Page boyutu
4096, girdi sayısı1000, B-tree derinliği2, kullanılan Page sayısı4vb. yer alır
- Örnek çıktıda Page boyutu
- Ancak bu araç, indeks içindeki Cell ve payload’u doğrudan incelemek için yalnızca özet bilgi düzeyinde kalır
- Birkaç haftalık deneyden sonra indeks analizi için fonksiyonlar yazıldı
- Kod:
sqlite.patch sqlite3DebugGetMemoryPayload(Mem *mem)sqlite3DebugGetCellPayloadAndRowId(BtCursor *pCur, MemPage * pPage, int cellIndex)sqlite3DebugBtreeIndexDump(BtCursor *pCur, int pageNumber)
- Kod:
- Bu fonksiyonlar seçilen indeks içeriğini okuyup STDOUT’a yazdırır
- Akış
SQL query -> selected index -> stdoutşeklindedir - Çıktıda Page numarası, sağ çocuk Page numarası, Cell numarası, sol çocuk Page numarası, payload ve rowId bulunur
- Akış
- Deney ortamı Docker ile çalıştırılabilir
docker run -it --rm -v "$PWD":/app/data --platform linux/x86_64 mrsuh/sqlite-index bashsh bin/dump-index.sh database.sqlite "SELECT * FROM table INDEXED BY index WHERE column=1" dump.txt
Görselleştirme yöntemindeki değişim
- Başta indeks yapısını görselleştirmek için d3-org-tree kullanıldı
- Ağaç derinleşip her seviyedeki Page sayısı arttıkça Page’ler arasındaki boşluğu ayarlamak zorlaştı; görüntü çok büyüdü ve okunması güçleşti
- JavaScript ve CSS ile ayarlamaya çalışıldı ancak istenen uyum sağlanamayınca bir süre metin tabanlı yapı gösterimine geçildi
- Metin çıktısı toplam Page sayısını, toplam Cell sayısını, seviye bazında Page·Cell sayılarını, Page bilgilerini, Cell bilgilerini ve payload’u gösterir
- Daha sonra PHP’nin ImageMagick eklentisi kullanılarak tasarım ve boşlukların daha hassas kontrol edilebildiği görsel çıktıya geliştirildi
- Nihai görsel şu bilgileri içerir
- Sol üstte indeksin genel bilgilerini gösterir
- Her seviyede toplam Page sayısını ve Cell sayısını gösterir
- Her Page için Page numarasını, sağ çocuk bağlantısını, ilk Cell ve son Cell bilgilerini gösterir
- Her seviyede Page’lerin yalnızca bir kısmını gösterir; ilk Page ve son Page dahildir
- Kök Page ilk seviyede yer alır
- Dökümden görsel üretme komutu şöyledir
php bin/console app:render-index --dumpIndexPath=dump.txt --outputImagePath=image.webp
Kayıt sayısının indeks biçimini değiştirmesi
column1 INT NOT NULLtablosundacolumn1 ASCindeksi oluşturulup kayıt sayısı değiştirilerek yapı incelendi- 1 kayıtlık indeks 1 seviye, 1 Page ve 1 Cell’den oluşur
- 1.000 kayıtlık indeks de aynı yöntemle oluşturulup görselleştirildi
- 1.000.000 kayıtlık indeks şu yapıya sahiptir
- 3 seviye
- 2.930 Pages
- 1.000.000 Cells
- Veriler sırayla eklendiği için
rowId = 1olduğundacolumn1 = 1’dir
Sıralama yönü ve ifade indeksleri
- Aynı veriler üzerinde
idx_ascveidx_descoluşturularak ASC/DESC indeksleri karşılaştırıldı - ASC indeksinde varsayılan sıralama ASC olduğundan önceki indeksle aynıdır
rowId=1,000,000,column1=1,000,000,payload=1,000,000olan öğe sağ uçtaki Page’in son Cell’inde bulunurrowId=1,column1=1,payload=1olan öğe sol uçtaki Page’in ilk Cell’inde bulunur
- DESC indeksinde yerleşim tersinedir
rowId=1,column1=1,payload=1olan öğe sağ uçtaki Page’in son Cell’inde bulunurrowId=1,000,000,column1=1,000,000,payload=1,000,000olan öğe sol uçtaki Page’in ilk Cell’inde bulunur
- İfade tabanlı indeks ifadenin ürettiği dizeyi saklar
- Örnekte JSON metninden
$.timestampçıkarılıpstrftime('%Y-%m-%d %H:%M:%S', ..., 'unixepoch')ile dönüştürülerek ASC indeks oluşturulur - Daha karmaşık ifadeler de kullanılabilir; indekste yalnızca sonuçları saklanır
- Örnekte JSON metninden
NULL, Partial Index, çoklu sütun
- SQLite, NULL değerleri içeren UNIQUE indeksleri destekler
- Örnekte
1, çok sayıdaNULL,1000000değeri eklenipCREATE UNIQUE INDEX idx ON table_test (column1 ASC)çalıştırılır - Görselleştirilen indeks yalnızca NULL olmayan değerleri saklıyor gibi görünür
- Örnekte
WHERE column1 IS NOT NULLkoşulu eklenen Partial Index, NULL değerlerini filtreler- Bu indeks yalnızca bir Page içerir
- Önceki UNIQUE örneğine göre daha hızlı arama sağlar
- Çoklu sütun indeksi, tüm alan verilerini Cell içinde sırayla saklar
- Örnek
(column1 ASC, column2 ASC)indeksidir - Görselleştirmede alanlar iki nokta
:ile ayrılır
- Örnek
İndeks oluşturma zamanı ve yeniden yapılandırmanın etkisi
- İndeksi verileri eklemeden önce oluşturma ile tüm veriler eklendikten sonra oluşturma karşılaştırıldı
- Yeni veri eklendiğinde ağaç kendi kendini yeniden dengelemek zorundadır
- Mevcut veriler için indeksi tek seferde oluşturmak çok daha verimli olabilir
- İki indeks benzer görünse de Page sayısı daha az olan ikinci indeks daha hızlı olabilir
- 1.000.000 Cells bazında karşılaştırma sonucu şöyledir
| Tür | Total Pages | Total Cells |
|---|---|---|
| Eklemeden önce oluşturma | 3342 | 1000000 |
| Eklemeden sonra oluşturma | 2930 | 1000000 |
- Benzer optimizasyon VACUUM veya REINDEX ile yapılabilir
VACUUM, indeksleri ve tabloları verilerle birlikte yeniden oluştururREINDEX idx, yalnızca indeksi yeniden oluşturur
- Her iki komut da örnekte Page sayısını 3342’den 2930’a düşürdü
Veri tipine göre indeks saklama
- Metin verileri için kısa dizeler doğrudan indeks Cell’inde saklanır, ancak uzun metinlerin ayrı saklanması gerekir
- Örnekte
text-1dentext-1000000e kadar değerler eklenipcolumn1 ASCindeksi oluşturulur - Gerçek dizelerin doğrudan indekste saklandığı görülebilir
- Örnekte
- REAL verileri de indekste saklanıp görselleştirildi
- Örnek
1.14,2.14, ...,1000000.14değerlerini kullanır
- Örnek
- Tamsayı ve metni birlikte kullanan bileşik indeks de incelendi
- Örnekte
(column1 INT, column2 TEXT)tablosunda(column1 ASC, column2 ASC)indeksi oluşturulur - Tamsayı ve dize, indeks oluşturulurken belirtildiği şekilde aynı Cell içinde birlikte saklanır
- Örnekte
Yeniden üretme yöntemi ve sonraki işler
- Deney, SQLite indekslerinin nasıl yapılandırıldığını, kayıt verilerinin bellekte nasıl saklandığını ve B-Tree’nin verileri nasıl organize edip eriştiğini gösterir
- Görselleştirme, farklı indeksleri analiz etmek ve karşılaştırmak için kullanılır
- Tüm örnekler şu komutla yeniden üretilebilir
docker run -it --rm -v "$PWD":/app/data --platform linux/x86_64 mrsuh/sqlite-index bashsh bin/test-index.sh
- Kod ve örnekler
mrsuh/sqlite-indexiçinde bulunur - Sonraki işler indeks tabanlı arama görselleştirmesi ve birkaç SQL sorgusunu incelemektir
1 yorum
Hacker News yorumları
SQLite tablolarındaki her satırın varsayılan olarak benzersiz bir rowId’si olduğu ve açıkça tanımlanmış bir birincil anahtar yoksa birincil anahtar gibi davrandığı söylenmiş; ama pratikte birincil anahtar olsa bile rowid kullanılır
WITHOUT ROWIDtablolarının birincil anahtar indeksini görselleştirmek güzel olurdu. Bu tür indeksler özellikle ilginçİki indeks benzer görünse bile ikinci indeksin daha az sayfası olması, doğrudan daha hızlı olduğu anlamına gelmez. Önemli olan ağacın yüksekliğidir; ardından da indekste değeri bulduktan sonra kalan verilerin ayrı bir tablodan (rowid) okunmasının gerekip gerekmediği ya da
WITHOUT ROWID’de olduğu gibi verinin doğrudan orada bulunup bulunmadığı gelir. Özelliklewhere 50 <= col <= 100gibi aralık sorgularında fark büyüktürINTEGER PRIMARY KEYoluşturursanız SQLite onun yerine bunu kullanır [1][1]: https://sqlite.org/rowidtable.html
SQLite, neredeyse tüm işleme biçimlerinde oldukça kendine özgü; özellikle de sorgu işleme konusunda böyle olduğunu düşünüyorum
SQLite performanstan çok sadeliği tercih etme eğiliminde olduğundan, çalıştığım diğer veritabanlarından farklı şekillerde uygulanmış çok şey var. SQLite diğer veritabanlarıyla rekabet etmekten ziyade kalıcı depolama için kullanılan JSON/XML dosyalarıyla rekabet eder. Bu yüzden SQLite’ın uygulamasına bakmak, gerçek veritabanlarının aynı işleri nasıl yaptığı hakkında çok fazla şey öğretmez
Bu, gereksinimlerin epey farklı olduğu anlamına gelse de kullanım alanı yalnızca JSON/XML dosyalarının yerine geçmekle sınırlı değildir
Web sitesi o kadar okunaklı ki gerçekten okumak isteği uyandırıyor
“indexes”, “to index” fiilinin üçüncü tekil şahıs geniş zaman biçimi olduğu gibi “index”in çoğul isim biçimi de olabilir. Buna karşılık “indices” geleneksel çoğul biçimdir ve özellikle matematik ve bilim bağlamlarında sık kullanılır
Genel İngilizcede “indexes” yaygındır; ancak teknik alanlarda dilsel doğruluk adına indices tercih edilebiliyor. Bu bağlamda “indices” kullanmak, indeksleme işlemiyle indeksin çoğulunu ayırarak açıklığı artırır
Finlandiya’da çoğul olarak “time series”, tekil olarak “time serie” kullanıldığını gördüm
Başlıca ilişkisel veritabanı yönetim sistemlerinin hepsi indexes terimini kullanır
PostgreSQL’in aynı işi nasıl yaptığını da görmek güzel olurdu. Karşılaştırarak öğrenilecek çok şey var gibi
Daha az işle farklı yerleşimleri görmek için yEd’e yönelik TGF çıktısı üretmesi de iyi olabilir