Veri Bilimi Okulu

Pandas SQL: ADBC (Arrow Database Connectivity)
Pandas SQL: ADBC (Arrow Database Connectivity)
cover-1-pandas-adbc_960x640

Loading

Eğer veri analizinde pandas ile SQL veritabanları arasında köprü kuran biriyseniz, muhtemelen pd.read_sql() ve df.to_sql() çağrılarınızın kaplumbağa hızında çalıştığı, hatta veri tiplerinin (data types) yolda kaybolduğu anlar yaşamışsınızdır. İyi haber: pandas 2.2 ile birlikte gelen ADBC (Arrow Database Connectivity) sürücü desteği, bu hikayeyi tamamen değiştiriyor [1][2]. Bu yazıda yeni özelliği baştan sona inceleyeceğiz, neden bu kadar heyecanlı olmamız gerektiğini göreceğiz ve örneklerle pratik kullanımı yapacağız.

Önce Hızlı Bir Bağlam: Pandas SQL Tarihçesi

Pandas, uzun zamandır SQL veritabanlarıyla konuşabiliyor; ama bu konuşma genellikle SQLAlchemy köprüsü üzerinden yapılıyordu. SQLAlchemy harika bir kütüphane ama bir sorunu var: veritabanından gelen verileri satır satır (row-wise) okuyor, sonra bunları Python nesnelerine (Python objects) çeviriyor, en sonunda pandas bu nesneleri kendi sütun temelli (columnar) yapısına dönüştürüyor [3]. Yani araya birkaç dönüşüm katmanı giriyor ve hem hız hem de tip güvenliği (type safety) bundan zarar görüyor.

Eski tarz okumayı zaten biliyoruz:

import pandas as pd
from sqlalchemy import create_engine

engine = create_engine("postgresql://user:pass@localhost/mydb")
df = pd.read_sql("SELECT * FROM sales", engine)

Çalışıyor, evet — ama büyük tablolarda saatlerce bekleyebiliyorsunuz. Üstelik veritabanındaki INTEGER NULL sütunu, pandas tarafına geçince genellikle float64 oluyor çünkü NumPy tabanlı pandas eksik tam sayıları (nullable integers) yerel olarak desteklemiyor [4]. Veri mühendisleri (data engineers) için bu, alt akıştaki (downstream) tüm tip dönüşümlerini elle düzeltmek demek.

ADBC Tam Olarak Ne?

ADBC, açılımıyla Arrow Database Connectivity, Apache Arrow projesinin geliştirdiği yeni nesil bir veritabanı bağlantı standardı [5]. JDBC’yi ya da ODBC’yi düşünün — ADBC de benzer bir soyutlama (abstraction) sunar, ama temelde sütun temelli Apache Arrow formatını kullanır. Bu, üç pratik fark yaratır:

  1. Sıfır kopya (zero-copy) veri aktarımı: Veritabanından gelen Arrow bellek tamponları (memory buffers) doğrudan pandas DataFrame’e bağlanabilir [3][6].
  2. Zengin tip sistemi (type system): PostgreSQL’in NUMERIC, UUID, JSONB gibi tipleri kayba uğramadan taşınır [7].
  3. Hız: Satır satır dönüşüm yok; sütunlar olduğu gibi okunur.

Pandas 2.2 release notes’unda bu özellik şöyle özetleniyor: ADBC sürücüleri, geleneksel SQLAlchemy sürücüleriyle karşılaştırıldığında “önemli performans iyileştirmeleri, daha iyi tip desteği ve daha temiz null değer (nullability) yönetimi” sunuyor [1].

Performans Farkı: Sayılarla Konuşalım

Bunun ne kadar fark ettiğini görmek için topluluktan gelen ölçümlere bakalım. dlthub ekibinin yaptığı kıyaslamada (benchmark), 5 milyon satırlık veriyi DuckDB’den MySQL’e taşıma işi SQLAlchemy ile 344 saniye sürerken, Arrow + ADBC yoluyla 92 saniyeye düşmüş — yaklaşık 3.7 kat hızlanma [8]. Will Ayd’ın bağımsız PostgreSQL ölçümünde de hem okuma hem yazmada ADBC, SQLAlchemy’yi açık ara geçiyor; üstelik PostgreSQL satır temelli (row-oriented) bir veritabanı bile [6]. Sütun temelli (columnar) veritabanlarında — örneğin DuckDB, Snowflake, BigQuery — farkın daha da büyümesi bekleniyor.

SQLAlchemy’nin yavaşlığının arkasındaki temel sebep ise mimari: GitHub’da uzun süredir tartışılan #9436 sayılı sorunda gösterildiği üzere, SQLAlchemy executemany yolunda satır satır parametre bağlama (parameter binding) yapıyor ve bu, ölçeklendikçe darboğaza (bottleneck) dönüşüyor [9].

Kurulum: 5 Dakikada Sıfırdan Çalışan Bir Ortam

Hadi gerçek bir ortam kuralım. Hiçbir şey kurmadığımızı varsayıyoruz; sonunda elimizde çalışan bir PostgreSQL ve onunla konuşan bir Python betiği (Python script) olacak.

1. Adım: Docker ile PostgreSQL Konteyneri (Container) Ayağa Kaldıralım

Bilgisayarımıza PostgreSQL kurmak yerine Docker konteyneri kullanmak çok daha pratik — işimiz bitince docker rm ile siliveririz. Resmi PostgreSQL imajını (image) çekiyor ve arka planda (detached mode) başlatıyoruz:

docker run -d \
  --name pandas-pg \
  -e POSTGRES_USER=demo \
  -e POSTGRES_PASSWORD=demo \
  -e POSTGRES_DB=demodb \
  -p 5432:5432 \
  postgres:16

Komutu parçalayalım:

  • -d: konteyner arka planda çalışsın, terminali bloklamasın.
  • --name pandas-pg: konteynere isim veriyoruz ki sonra docker stop pandas-pg ile durdurabilelim.
  • -e POSTGRES_USER/PASSWORD/DB: PostgreSQL imajının resmi ortam değişkenleri (environment variables); kullanıcı, parola ve varsayılan veritabanını oluşturuyor.
  • -p 5432:5432: konteynerin 5432 portunu yerel makinemizin (localhost) 5432 portuna bağlıyoruz, böylece Python’dan localhost:5432 ile erişebileceğiz.
  • postgres:16: kullanacağımız imaj ve sürüm etiketi (tag).

Birkaç saniye sonra konteynerin hazır olduğunu doğrulayalım:

docker ps
docker logs pandas-pg | tail -n 5

database system is ready to accept connections satırını görüyorsak hazırız demektir.

2. Adım: Python Tarafındaki Paketleri Kuralım

Şimdi pandas, PyArrow ve PostgreSQL için ADBC sürücüsünü kuruyoruz [10][11]:

pip install --upgrade "pandas>=2.2" pyarrow adbc-driver-postgresql

PyArrow’un da kurulu olması gerekiyor — DBAPI uyumlu (DBAPI compliant) arayüz (interface) için bu zorunlu [12]. Pandas 3.0 ile PyArrow zaten zorunlu bağımlılık (required dependency) olacak, dolayısıyla bunu gelecek için bir yatırım sayabiliriz [1].

Diğer veritabanları için sürücüler de aynı PyPI mantığıyla kuruluyor:

pip install adbc-driver-sqlite       # SQLite için
pip install adbc-driver-snowflake    # Snowflake için
pip install adbc-driver-flightsql    # Flight SQL servisleri için

3. Adım: Bağlantıyı Test Edelim

Konteynerin gerçekten erişilebilir olduğunu kısa bir Python betiğiyle doğrulayalım:

import adbc_driver_postgresql.dbapi as pg_dbapi

uri = "postgresql://demo:demo@localhost:5432/demodb"

with pg_dbapi.connect(uri) as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT version();")
        print(cur.fetchone())
# ('PostgreSQL 16.x ...',)

PostgreSQL’in sürüm bilgisini gördüysek, her şey yolunda demektir.

İlk Örnek: PostgreSQL’e Yazma ve Geri Okuma

Hadi klasik bir senaryoya geçelim. Önce küçük bir tablo oluşturup yazalım, sonra geri okuyalım. Tek script halinde:

import pandas as pd
import adbc_driver_postgresql.dbapi as pg_dbapi

URI = "postgresql://demo:demo@localhost:5432/demodb"

# Örnek bir DataFrame
df = pd.DataFrame({
    "product": ["Notebook", "Klavye", "Mouse", "Monitör"],
    "price":   [25000.0, 850.0, 320.5, 7400.0],
    "stock":   [12, 45, 120, 8],
})

# Yazma
with pg_dbapi.connect(URI) as conn:
    df.to_sql("products", conn, if_exists="replace", index=False)
    conn.commit()

# Okuma
with pg_dbapi.connect(URI) as conn:
    df_back = pd.read_sql("SELECT * FROM products ORDER BY price DESC", conn)

print(df_back)

Dikkatinizi çekti mi: create_engine yok, SQLAlchemy import’u yok. Doğrudan ADBC sürücüsünü çağırıyoruz, pandas onu tanıyor ve verileri Arrow tablosu (Arrow table) olarak alıp DataFrame’e dönüştürüyor [1][13]. to_sql çağrısı da Arrow bellek tamponlarını doğrudan PostgreSQL’e yığın olarak (bulk load) yüklüyor; SQLAlchemy yolundaki method='multi' ya da fast_executemany=True gibi sihirli parametrelere artık ihtiyacımız yok [2][8][14].

Konteynerden Doğrulama

Docker konteynerine girip verilerin gerçekten yazıldığını gözlerimizle de görebiliriz:

docker exec -it pandas-pg psql -U demo -d demodb -c "SELECT * FROM products;"

Sihir Burada: dtype_backend="pyarrow" ile Sıfır Kopya

Asıl güzelliği görmek için bir adım daha atalım. read_sql çağrısında dtype_backend="pyarrow" parametresini eklediğimizde, pandas verileri geleneksel NumPy yerine PyArrow ile destekli sütunlar (PyArrow-backed columns) olarak tutar [1][13]:

with pg_dbapi.connect(URI) as conn:
    df = pd.read_sql(
        "SELECT * FROM products",
        conn,
        dtype_backend="pyarrow"
    )

print(df.dtypes)
# product    string[pyarrow]
# price      double[pyarrow]
# stock      int64[pyarrow]

Bu kombinasyon gerçek sıfır kopya (zero-copy) bir yolculuk demek. Veriler veritabanından Arrow tampona, oradan da DataFrame’e tek bir bellek bloğu olarak geçer. Will Ayd’ın belirttiği gibi, “veri tipleri veritabanı meta verisinden (metadata) doğrudan çıkarsanır” — yani INTEGER NULL artık float64 olmaz, Int64[pyarrow] olarak gelir, NULL’lar da pd.NA olur [6].

Tam Tur Örnek: Eksik Değerlerle Round-Trip

Şimdi eksik değerleri (missing values) gerçekçi şekilde içeren bir senaryo deneyelim — ADBC’nin tip korumadaki gerçek farkı burada parlıyor:

import pandas as pd
import adbc_driver_postgresql.dbapi as pg_dbapi

URI = "postgresql://demo:demo@localhost:5432/demodb"

# Eksik değerli bir DataFrame
df = pd.DataFrame({
    "id": [1, 2, 3, 4],
    "customer": ["Ayşe", "Mehmet", None, "Fatma"],
    "amount": [99.5, None, 45.0, 220.75],
    "is_paid": [True, False, True, None],
})

# Yazma
with pg_dbapi.connect(URI) as conn:
    df.to_sql("orders", conn, if_exists="replace", index=False)
    conn.commit()

# Okuma - pyarrow backend ile
with pg_dbapi.connect(URI) as conn:
    df_back = pd.read_sql(
        "SELECT * FROM orders ORDER BY id",
        conn,
        dtype_backend="pyarrow"
    )

print(df_back)
print()
print(df_back.dtypes)
# id          int64[pyarrow]
# customer    string[pyarrow]
# amount      double[pyarrow]
# is_paid     bool[pyarrow]

SQLAlchemy yolunda is_paid sütunundaki None değeri yüzünden tüm sütun object tipine düşerdi; amount sütunu da NaN içerdiği için float64 olarak gelirdi. ADBC + pyarrow backend ile tipler temiz kalıyor ve eksik değerler pd.NA olarak doğru biçimde gösteriliyor [1][6].

Temizlik: Konteyneri Kapatmak

Denemelerimiz bittiğinde konteyneri silmeyi unutmayalım:

docker stop pandas-pg
docker rm pandas-pg

Yarın yeniden başlamak istersek aynı docker run komutu sıfırdan temiz bir PostgreSQL hazırlar.

Ne Zaman ADBC, Ne Zaman SQLAlchemy?

Yeni özellik harika ama her zaman uygun olmayabilir. Kabaca bir karar rehberi yapacak olursak:

ADBC’yi tercih edin — büyük tablolar okuyup yazıyorsanız, PostgreSQL/SQLite/Snowflake/DuckDB gibi desteklenen bir motor kullanıyorsanız, tip korunmasını (type preservation) önemsiyorsanız ya da PyArrow destekli pandas iş akışına geçiyorsanız [6][11].

SQLAlchemy’de kalın — şu anda ADBC sürücüsü olmayan bir veritabanı kullanıyorsanız (örneğin MS SQL Server için hâlâ olgun bir ADBC sürücüsü yok), ORM (Object-Relational Mapping) özelliklerinden yararlanıyorsanız, ya da projenizin geri kalanı SQLAlchemy üzerine kuruluysa [9][15]. SQLAlchemy yolu hâlâ resmi olarak destekleniyor, kaldırılmayacak.

ADBC sürücüleri hâlâ aktif geliştirme aşamasında; resmi durum tablosunu Apache Arrow ADBC dokümantasyonunda takip edebiliriz [16].

Dikkat Edilmesi Gereken Bazı Noktalar

Birkaç ufak tuzak da görelim ki şaşırmayalım:

  • Bağlantıyı (connection) doğru kapatın: ADBC bağlantıları context manager (with bloğu) içinde kullanılmadığında bellek sızıntısı (memory leak) yapabilir [12]. Her zaman with bloğu içinde tutalım.
  • SQL Server kullanıcıları için henüz bekleme zamanı: Mevcut topluluk kıyaslamalarına bakılırsa, SQL Server hâlâ ODBC + fast_executemany=True ya da mssql-python üzerinden gidiyor [17][18].
  • PyArrow zorunlu: DBAPI arayüzünü kullanmak için PyArrow ya da Polars’ın kurulu olması gerekiyor [12].
  • pandasql ile karıştırmayalım: Bazı eski blog yazılarında “pandas’ta SQL” denildiğinde aslında DataFrame’ler üzerine SQL sorgusu çalıştıran pandasql paketi kastediliyor [19]. Bizim konuştuğumuz şey çok farklı; ADBC, DataFrame’lere SQL söz dizimi (SQL syntax) eklemiyor, gerçek veritabanı bağlantısını hızlandırıyor.

Bonus: Pandas 2.2’nin Yan Hediyesi case_when

ADBC’nin yanı sıra pandas 2.2, SQL’den pandas’a geçenlerin uzun süredir özlediği bir başka şeyi de getirdi: Series.case_when [2]. SQL’deki CASE WHEN ifadesinin pandas eşdeğeri:

df = pd.DataFrame(dict(a=[1, 2, 3], b=[4, 5, 6]))
default = pd.Series('default', index=df.index)

result = default.case_when(
    caselist=[
        (df.a == 1, 'first'),
        (df.a.gt(1) & df.b.eq(5), 'second'),
    ],
)

Bu, koşullu sütun oluştururken çoklu np.where zincirlerine veya apply çağrılarına olan ihtiyacı büyük ölçüde ortadan kaldırıyor [2][20].

Sonuç

Pandas 2.2 ile gelen ADBC desteği, kütüphane için sessiz bir devrim sayılır. Yıllardır pandas-SQL etkileşiminde takıldığımız iki temel sorunu — yavaşlık ve tip kaybı — aynı anda çözüyor [1][6]. PyArrow destekli iş akışına geçiş planımız varsa, ya da büyük veritabanlarıyla çalışıyorsak, bugün denemeye değer bir özellik. Ben sizin yerinizde olsam bir sonraki ETL boru hattımı (ETL pipeline) yazarken doğrudan ADBC ile başlardım. Hem performans, hem tip güvenliği, hem de daha az kod — kötü bir takas değil bence.

Bir sonraki yazıda muhtemelen pandas 3.0 ile zorunlu hale gelecek Copy-on-Write konusuna gireceğiz; takipte kalın!


Kaynaklar

[1] What’s new in 2.2.0 — pandas documentation: https://pandas.pydata.org/docs/dev/whatsnew/v2.2.0.html

[2] Hoefler, P. — What’s New in Pandas 2.2, Towards Data Science: https://towardsdatascience.com/whats-new-in-pandas-2-2-e3afe6f341f5/

[3] pandas.read_sql — pandas 3.0.3 documentation: https://pandas.pydata.org/docs/reference/api/pandas.read_sql.html

[4] pandas.DataFrame.to_sql — pandas 3.0.3 documentation: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_sql.html

[5] Apache Arrow ADBC — Project Home: https://arrow.apache.org/adbc/current/index.html

[6] Ayd, W. — Leveraging the ADBC driver in Analytics Workflows: https://willayd.com/leveraging-the-adbc-driver-in-analytics-workflows.html

[7] ADBC Driver Implementation Status — Apache Arrow: https://arrow.apache.org/adbc/current/driver/status.html

[8] dltHub Engineering Blog — 3.7x Faster Pipelines: Benchmarking Arrow & ADBC vs. SQLAlchemy: https://dlthub.com/blog/arrow-adbc-vs-sqlalchemy

[9] SQLAlchemy GitHub Discussion #9436 — Slow Pandas to_sql with mssql+pyodbc: https://github.com/sqlalchemy/sqlalchemy/discussions/9436

[10] ADBC Installation — Apache Arrow: https://arrow.apache.org/adbc/current/driver/installation.html

[11] adbc-driver-flightsql — PyPI: https://pypi.org/project/adbc-driver-flightsql/

[12] Driver Manager — Apache Arrow ADBC documentation: https://arrow.apache.org/adbc/current/python/driver_manager.html

[13] pandas.read_sql — pandas 2.2.2 documentation: https://pandas.pydata.org/pandas-docs/version/2.2.2/reference/api/pandas.read_sql.html

[14] pandas.DataFrame.to_sql — pandas 2.2.3 documentation: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_sql.html

[15] Hoefler, P. — What’s new in pandas 2.2 (personal blog): https://phofl.github.io/pandas-whatsnew-22.html

[16] Apache Arrow ADBC Quickstart: https://arrow.apache.org/adbc/current/python/quickstart.html

[17] Leblanc, F. — Benchmarks for writing pandas DataFrames to SQL Server: https://leblancfg.com/benchmarks_writing_pandas_dataframe_SQL_Server.html

[18] MSSQLTips — Benchmark of Python Drivers for SQL Server: https://www.mssqltips.com/sqlservertip/11607/benchmark-of-python-drivers-for-sql-server/

[19] DataCamp — How to Use SQL in pandas Using pandasql Queries: https://www.datacamp.com/tutorial/how-to-use-sql-in-pandas-using-pandasql-queries

[20] What’s New in 2.2.0 — pandas raw release notes (whatsnew rst): https://pandas.pydata.org/docs/dev/_sources/whatsnew/v2.2.0.rst.txt

0

Bir yanıt yazın

Password Requirements:

  • At least 8 characters
  • At least 1 lowercase letter
  • At least 1 uppercase letter
  • At least 1 numerical number
  • At least 1 special character