Ana içeriğe geç
Kesit SQL
İleri · Veri Mühendisliği

Büyük veri: partition, clustering, dosya formatları

Petabayt ölçeğinde SQL'in hızlı ve ucuz çalışması.

Uygulamada aç, 10 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Partitioning (bölümleme)

Tablo, bir sütuna (çoğunlukla tarih) göre fiziksel parçalara ayrılır. WHERE event_date = '2024-12-01' yazdığında motor sadece o günün bölümünü okur (partition pruning).

-- BigQuery
CREATE TABLE analytics.events
PARTITION BY DATE(event_time)
CLUSTER BY user_id, event_type AS
SELECT * FROM raw.events;

Clustering / sıralama anahtarı

Bölüm içindeki veriyi belirli sütunlara göre sıralı tutar; filtrelenen bloklar atlanır (Snowflake micro-partition, Redshift sort key, Delta Z-order).

Dosya formatları

FormatYapıNot
CSV / JSONSatır, metinİnsan okur, büyük ve yavaş
Parquet / ORCSütun tabanlı, sıkıştırılmış, şemalıAnalitik için standart
AvroSatır tabanlı, şemalıAkış ve şema evrimi

Maliyet bilinci

Derinleş: bölüm budamasını gerçekten çalıştırmak

Bölüm budaması (partition pruning), sorgu motorunun filtreye bakarak okunması gereksiz bölümleri hiç açmamasıdır. Bulut ambarlarında taranan bayt faturalandığı için bu doğrudan para demektir: 3 yıllık olay tablosunda bir günlük sorgu, budama çalışırsa verinin yaklaşık 1/1000'ini okur.

Budamayı bozan yazımlar

Budama, filtre bölümleme sütununun kendisi üzerinde ve sabit değerlerle yazıldığında çalışır. Sütuna fonksiyon uygularsan motor çoğu durumda hangi bölümlere bakacağını çıkaramaz:

-- Budama dostu değil: sütun fonksiyonun içinde
SELECT COUNT(*) AS adet, ROUND(SUM(amount)) AS toplam
FROM bank_txns
WHERE strftime('%Y-%m', txn_time) = '2024-03';

-- Budama dostu: sütun yalın, sınırlar sabit
SELECT COUNT(*) AS adet, ROUND(SUM(amount)) AS toplam
FROM bank_txns
WHERE txn_time >= '2024-03-01' AND txn_time < '2024-04-01';

İki sorgu da aynı sonucu (3.510 işlem) verir; ama bölümlenmiş bir tabloda ilki tüm bölümleri tarar. Aynı ilke indeksler için de geçerlidir (sargable koşul). Benzer tuzaklar: CAST(tarih AS STRING) LIKE '2024%', alt sorgudan gelen dinamik tarih (bazı motorlar budayamaz), bölüm sütununu hiç filtrelememek.

Bölüm büyüklüğünü seçmek

SELECT COUNT(*) AS satir,
       COUNT(DISTINCT date(txn_time)) AS gunluk_bolum,
       COUNT(DISTINCT strftime('%Y-%m', txn_time)) AS aylik_bolum,
       ROUND(1.0 * COUNT(*) / COUNT(DISTINCT date(txn_time))) AS gun_basina_satir
FROM bank_txns;

Ilgaz Bank'ta 2024'te 42.284 işlem var: günlük bölümlemede 366 bölüm ve bölüm başına yaklaşık 116 satır. Bu, gerçek bir ambar için fazla küçük bir bölümdür; her bölümün dosya ve meta veri maliyeti olduğundan çok sayıda küçük bölüm sorguyu yavaşlatır. Kural olarak bölüm başına yüzlerce MB – birkaç GB hedeflenir; küçük tablolarda aylık bölüm ya da hiç bölümlememek daha iyidir.

TeknikNe yapar?İyi olduğu filtre
PartitionVeriyi fiziksel parçalara bölerTarih aralığı
Clustering / sort keyBölüm içinde sıralı tutar, blok atlarYüksek kardinaliteli eşitlik (user_id)
İndeks (OLTP)Satır konumuna hızlı erişimAz sayıda satır döndüren arama
Materialized viewSonucu önceden hesaplarSık tekrarlanan ağır toplama
Sıcak bölümBölümler eşit büyüklükte olmayabilir. Ilgaz Bank'ta en kalabalık günler ay sonlarıdır (31 Aralık 201 işlem). Ay sonu raporları bu bölümlere yüklenir; kapasite planlamasında ortalamaya değil en büyük bölüme bak.

Bölüm eleme (partition pruning): gerçek çıktı

bank_txns verisini PostgreSQL'de çeyreklere bölünmüş bir tabloya aldık (tx_bolum). Aşağıdakiler PostgreSQL 16.4 üzerinde üretilen gerçek planlardır.

Bölüm sütunu yalın: tek bölüm okunur

EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, SUMMARY OFF)
SELECT COUNT(*) FROM tx_bolum WHERE txn_time >= '2024-11-01' AND txn_time < '2024-12-01';

Gerçek çıktı (PostgreSQL 16.4, PGlite)

Aggregate  (cost=270.09..270.10 rows=1 width=8) (actual rows=1 loops=1)
  Buffers: shared hit=100
  ->  Seq Scan on tx_2024q4 tx_bolum  (cost=0.00..261.09 rows=3600 width=0) (actual rows=3590 loops=1)
        Filter: ((txn_time >= '2024-11-01 00:00:00'::timestamp without time zone) AND (txn_time < '2024-12-01 00:00:00'::timestamp without time zone))
        Rows Removed by Filter: 7149
        Buffers: shared hit=100

Sütun fonksiyonda: bütün bölümler okunur

EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, SUMMARY OFF)
SELECT COUNT(*) FROM tx_bolum WHERE EXTRACT(MONTH FROM txn_time) = 11;

Gerçek çıktı (PostgreSQL 16.4, PGlite)

Aggregate  (cost=1027.84..1027.85 rows=1 width=8) (actual rows=1 loops=1)
  Buffers: shared hit=392
  ->  Append  (cost=0.00..1027.32 rows=211 width=0) (actual rows=3590 loops=1)
        Buffers: shared hit=392
        ->  Seq Scan on tx_2024q1 tx_bolum_1  (cost=0.00..251.73 rows=52 width=0) (actual rows=0 loops=1)
              Filter: (EXTRACT(month FROM txn_time) = '11'::numeric)
              Rows Removed by Filter: 10382
              Buffers: shared hit=96
        ->  Seq Scan on tx_2024q2 tx_bolum_2  (cost=0.00..254.42 rows=52 width=0) (actual rows=0 loops=1)
              Filter: (EXTRACT(month FROM txn_time) = '11'::numeric)
              Rows Removed by Filter: 10495
              Buffers: shared hit=97
        ->  Seq Scan on tx_2024q3 tx_bolum_3  (cost=0.00..259.02 rows=53 width=0) (actual rows=0 loops=1)
              Filter: (EXTRACT(month FROM txn_time) = '11'::numeric)
              Rows Removed by Filter: 10668
              Buffers: shared hit=99
        ->  Seq Scan on tx_2024q4 tx_bolum_4  (cost=0.00..261.09 rows=54 width=0) (actual rows=3590 loops=1)
              Filter: (EXTRACT(month FROM txn_time) = '11'::numeric)
              Rows Removed by Filter: 7149
              Buffers: shared hit=100

Aynı sonuç, ama 100 yerine 392 sayfa.

Alt sorgudan gelen tarih: çalışma anında eleme

EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, SUMMARY OFF)
SELECT COUNT(*) FROM tx_bolum WHERE txn_time >= (SELECT MAX(txn_time) - INTERVAL '30 days' FROM bank_txns);

Gerçek çıktı (PostgreSQL 16.4, PGlite)

Aggregate  (cost=1945.83..1945.84 rows=1 width=8) (actual rows=1 loops=1)
  Buffers: shared hit=491
  InitPlan 1 (returns $0)
    ->  Aggregate  (cost=919.55..919.56 rows=1 width=8) (actual rows=1 loops=1)
          Buffers: shared hit=391
          ->  Seq Scan on bank_txns  (cost=0.00..813.84 rows=42284 width=8) (actual rows=42284 loops=1)
                Buffers: shared hit=391
  ->  Append  (cost=0.00..991.02 rows=14095 width=0) (actual rows=3462 loops=1)
        Buffers: shared hit=491
        ->  Seq Scan on tx_2024q1 tx_bolum_1  (cost=0.00..225.78 rows=3461 width=0) (never executed)
              Filter: (txn_time >= $0)
        ->  Seq Scan on tx_2024q2 tx_bolum_2  (cost=0.00..228.19 rows=3498 width=0) (never executed)
              Filter: (txn_time >= $0)
        ->  Seq Scan on tx_2024q3 tx_bolum_3  (cost=0.00..232.35 rows=3556 width=0) (never executed)
              Filter: (txn_time >= $0)
        ->  Seq Scan on tx_2024q4 tx_bolum_4  (cost=0.00..234.24 rows=3580 width=0) (actual rows=3462 loops=1)
              Filter: (txn_time >= $0)
              Rows Removed by Filter: 7277
              Buffers: shared hit=100

PostgreSQL değeri planlama sırasında bilmiyor, ama çalışırken bölümleri eliyor. Bu yüzden diğer bölümler never executed yazıyor. Motordan motora değişen konu budur: BigQuery belgesine göre alt sorgu sonucuyla karşılaştırma bölüm elemez, tarih sabit bir ifade olmalıdır.

BigQuery bölüm kuralları (belgeden)

SQLite'ın bölümleme özelliği yok. Bu yüzden görevlerde mantığı aralık koşulu ve indeksle çalıştırıyoruz, gerçek bölüm davranışını yukarıdaki çıktılardan okuyoruz.

Alıştırmalara geç (10)