PostgreSQL Array Tipleri ve Sorgular
Selamlar, bu yazımda PostgreSQL'in çoğu zaman göz ardı edilen ama doğru yerde kullanıldığında ekstra tablolardan kurtaran array veri tipine bakacağız. ORM'ler bu tipi nadiren first-class vatandaş olarak gördüğü için biraz unutulmuş bir özellik gibi geliyor; oysa etiket, telefon numarası, kısa permission listesi gibi yerlerde junction table açmaktan çok daha pratik. Hadi başlayalım.
Array nedir, ne zaman tercih edilir?
PostgreSQL'de bir kolon, herhangi bir scalar tipin dizisi (array) olabilir: TEXT[], INTEGER[], hatta çok boyutlu INTEGER[][]. Yani aynı kolonda birden fazla değer tutabiliyorsunuz, ayrıca bunları sorgulayabileceğiniz operatör seti de epey zengin.
Peki her duruma uygun mu? Bence şu basit kontrol işe yarıyor:
- Eleman sayısı küçük ve nispeten sabit mi (etiketler, kısa rol listeleri, telefon numaraları)? Array uygun.
- Elemanların kendi başına foreign key, ayrı kolonlar veya per-eleman timestamp gibi metadata'sı var mı? Array uygun değil, junction table açın.
- İç içe karmaşık yapı mı tutacaksınız?
JSONBdaha doğru cevap.
Şahsi kanaatim, tags kolonu için array; comments ilişkisi için ayrı tablo. Bu sınır net olduğunda hayat kolaylaşıyor.
Tablo, ekleme, erişim
Hadi minimum bir örnekle bakalım. Bir articles tablosu kuruyoruz:
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
tags TEXT[], -- metin dizisi
ratings INTEGER[], -- tam sayi dizisi
published_dates DATE[]
);
Veri eklerken iki yazım var. İlki literal sözdizimi, ikincisi ARRAY constructor; ben okunabilirliği için ikincisini tercih ediyorum:
-- literal yazim
INSERT INTO articles (title, tags, ratings)
VALUES ('PostgreSQL Rehberi', '{postgresql,veritabani,sql}', '{5,4,5}');
-- ARRAY constructor (daha okunakli)
INSERT INTO articles (title, tags, ratings)
VALUES ('Bir Baska Yazi', ARRAY['tech', 'programlama'], ARRAY[4, 3, 5]);
Erişim 1-indexli, buraya dikkat: tags[1] ilk eleman demek. Slice de var, tags[1:2] ilk iki elemanı veriyor. Negatif indeks yok; son elemana ulaşmak için tags[array_length(tags, 1)] yazıyoruz. Biraz çirkin, evet.
Operatörler ve sorgular
İşin asıl gücü buradaki üç operatörde gizli: @> (içerir), <@ (içerilir), && (kesişir). Bir de eski güzel ANY var.
-- 'postgresql' etiketini iceren yazilar
SELECT * FROM articles WHERE tags @> ARRAY['postgresql'];
-- 'postgresql' veya 'mysql' etiketinden herhangi biri varsa
SELECT * FROM articles WHERE tags && ARRAY['postgresql', 'mysql'];
-- her iki etiketi de tasiyorsa
SELECT * FROM articles WHERE tags @> ARRAY['postgresql', 'veritabani'];
-- tek deger kontrolu
SELECT * FROM articles WHERE 'postgresql' = ANY(tags);
Buradaki kritik nokta şu: = ANY(tags) ile tags @> ARRAY['postgresql'] mantıken aynı şeyi yapıyor ama indeks açısından eşit değiller. GIN indeksi @> ve && ile çalışırken, = ANY query planner'a göre bazen indekse uğramayabiliyor. Tecrübem o yönde, planner'a güvenmeyin, EXPLAIN ANALYZE çekin.
Toparlama tarafında da array_agg çok işe yarıyor:
-- aktif kullanicilarin isimlerini tek diziye topla
SELECT array_agg(name ORDER BY created_at DESC)
FROM users WHERE active = true;
-- diziyi satirlara ac
SELECT id, unnest(tags) AS tag FROM articles;
unnest, array sorgularında en sık başvurduğum fonksiyon. Tag bulutu, frekans sayımı, distinct etiket listesi - hepsi bunun üzerine kurulu.
GIN indeksi olmadan olmaz
Birkaç bin satırda her şey hızlı görünür. Yüz binlere çıktığınızda containment sorgusu sequential scan'e döner ve canınız yanar. Çare basit:
CREATE INDEX idx_articles_tags ON articles USING GIN(tags);
Sonra EXPLAIN ANALYZE ile Bitmap Index Scan on idx_articles_tags satırını görmelisiniz. GiST de bir alternatif, daha küçük ama daha yavaş; tag aramaları için ben GIN'i öneririm.
İfade indeksi de bazen lazım oluyor. Mesela 'üçten fazla etiketi olan yazılar' sorgusu için:
CREATE INDEX idx_articles_tag_count ON articles(array_length(tags, 1));
Sık karşılaşılan tuzaklar
- Tek elemanı in-place güncellemek:
UPDATE articles SET tags[1] = 'yeni' WHERE id = 1çalışır gibi görünür ama PostgreSQL her durumda tüm array'i yeniden yazar. Bunun yerinearray_replace,array_append,array_removekullanın; niyetiniz daha okunaklı olur. - GIN indeksini unutmak: Containment sorgusu var ve indeks yoksa veri büyüdükçe yavaşlama lineer değil, hissedilir. Tablo 10 bin satıra ulaştığında indeksi atın, sonra atmadığınıza pişman olmayın.
- Array'i ilişki yerine kullanmak: Elemanların kendi metadata'sı varsa (oluşturulma tarihi, kim ekledi, ayrı silinebilir mi) array değil, ayrı tablo. 'Az kalsın junction'dan kurtulurum' diye başlayan her hikâye kötü bitiyor.
= ANYile indeks beklentisi: Çalışıyor ama planner her zaman indekse gitmiyor. Performans hassassa@>tercih edin.- Aşırı büyük array'ler: Bir satırda yüzlerce eleman tutmaya başladığınızda hem TOAST'a düşersiniz hem de update maliyeti patlar. Sınır koyun.
Doğrulama
Lokalde Docker ile bir Postgres ayağa kaldırın, küçük bir tablo doldurun ve EXPLAIN ANALYZE ile gerçekten indeksin devreye girip girmediğine bakın:
docker run -d --name pg-test -e POSTGRES_PASSWORD=test -p 5432:5432 postgres:16
psql -h localhost -U postgres -c "EXPLAIN ANALYZE SELECT * FROM articles WHERE tags @> ARRAY['postgresql'];"
Çıktıda Bitmap Index Scan görüyorsanız işiniz tamam; Seq Scan görüyorsanız ya indeks eksik ya da tablo henüz çok küçük (planner küçük tablolarda zaten sequential tercih eder).
Kapanış
Bu yazıda PostgreSQL array tipinin kullanımına, operatörlerine ve indekslemenin neden hayati olduğuna baktık. Bence array, etiket ve kısa liste senaryolarında junction table'dan çok daha pratik; ama ilişkinin kendi metadata'sı varsa zorlamayın, ayrı tablo açın. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.
