Veri Bilimi Okulu

Pandas DataFrame'lerini SQL ile Sorgulamak: DuckDB, query() ve Ötesi
Pandas DataFrame’lerini SQL ile Sorgulamak: DuckDB, query() ve Ötesi
cover-2-sql-on-dataframes_960x640

Loading

Önceki yazımızda pandas’ın veritabanlarıyla nasıl konuştuğunu (ADBC sürücüleri) gördük. Peki ya tersi? Elimizde zaten bir DataFrame var ve onu doğrudan SQL ile sorgulamak istiyoruz — filtreleme (filtering), dönüştürme (transformation), toplulaştırma (aggregation), birleştirme (join). Bu mümkün mü? Hem de fazlasıyla. Bu yazıda pandas sql kullanımı konusunda mevcut yolları öğreneceğiz, hangisinin hangi durumda en iyi olduğunu göreceğiz ve gerçek örneklerle deneyeceğiz.

Spoiler: 2026’da herkesin konuştuğu cevap DuckDB ama tek seçenek o değil. Pandas’ın kendi query() ve eval() metodları, eski klasik pandasql paketi de hâlâ hayatta. Hepsine sırayla bakalım.

Önce Hızlı Bir Karar Tablosu

AraçSQL Tam DesteğiHızKurulumNe Zaman?
df.query()Hayır (SQL-vari)Çok hızlıYok (pandas içinde)Hızlı filtreleme
df.eval()Hayır (ifade)Çok hızlıYokSütun türetme (column derivation)
pandasqlEvet (SQLite)Çok yavaşpip installSadece eski projeler
DuckDBEvet (tam)Çok hızlıpip installModern, tavsiye edilen yol

Tabloda gördüğümüz gibi DuckDB öne çıkıyor, ama her durumun kendi tatlı noktası var. Hadi tek tek bakalım.

Yol 1: Pandas’ın Kendi query() Metodu

Pandas’ın DataFrame.query() metodu, SQL’in WHERE cümlesine en yakın yerel (native) seçenek. Tam SQL değil ama SQL’e alışkın gözler için çok okunaklı bir filtreleme dili sunuyor [1][2].

import pandas as pd

df = pd.DataFrame({
    "product": ["Laptop", "Klavye", "Mouse", "Monitör", "Tablet"],
    "price":   [25000, 850, 320, 7400, 12500],
    "stock":   [12, 45, 120, 8, 30],
    "category": ["electronics", "accessories", "accessories", "electronics", "electronics"],
})

# SQL'de:  SELECT * FROM df WHERE price > 1000 AND stock < 30
result = df.query("price > 1000 and stock < 30")
print(result)

Çıktı:

   product  price  stock     category
0   Laptop  25000     12  electronics
3  Monitör   7400      8  electronics

Birkaç güzel detay [3][4]:

  • Harici değişken (external variable): @ işaretiyle Python değişkenlerine erişebiliriz. threshold = 1000df.query("price > @threshold")
  • in / not in operatörleri: SQL’deki gibi çalışıyor. df.query("category in ['electronics', 'books']")
  • Boşluklu sütun adları: Geri tırnak (backtick) ile sarılır. df.query("`unit price` > 100")

query(), NumExpr motoru (engine) sayesinde büyük DataFrame’lerde klasik boolean maskelerden (boolean indexing) daha hızlı çalışıyor; çünkü ifadeyi parçalara ayırıp ara değişkenler yaratmak yerine tek seferde değerlendiriyor [5][6].

Kısıtı: query() sadece filtreleme için. Toplulaştırma, join, window function gibi şeyler yok.

Yol 2: eval() ile Hesaplama ve Sütun Türetme

Yeni sütun yaratmak veya hesaplama yapmak istediğimizde DataFrame.eval() sahneye çıkıyor [5][6]:

# SQL'de:  SELECT *, price * stock AS total_value FROM df
df_with_value = df.eval("total_value = price * stock", inplace=False)
print(df_with_value)

Çıktı:

   product  price  stock     category  total_value
0   Laptop  25000     12  electronics       300000
1   Klavye    850     45  accessories        38250
...

eval() de NumExpr destekli; özellikle büyük veride zincirlenmiş aritmetik ifadelerde belirgin bir hız kazandırıyor [5]. Ama yine SQL’in tamamını değil, sadece bir alt kümesini sunuyor.

Yol 3: pandasql — Klasik Ama Yavaş

pandasql, DataFrame’lere doğrudan SQL yazmamızı sağlayan en eski paketlerden biri [7]. Altta SQLite kullanıyor:

from pandasql import sqldf

query = """
    SELECT category, AVG(price) AS avg_price
    FROM df
    WHERE stock > 10
    GROUP BY category
"""
result = sqldf(query, locals())

Çalışıyor, evet — ama bir sorunu var. DuckDB ekibinin yaptığı resmi kıyaslamada (benchmark), aynı sorgu üzerinde pandasql DuckDB’den yaklaşık 1000 kat daha yavaş çıktı [8]. Sebebi mimari: pandasql her sorguda DataFrame’i SQLite’a INSERT INTO ifadeleriyle yüklüyor, sorguyu çalıştırıyor, sonra sonucu geri okuyor. Bu round-trip büyük veride boğucu.

Sonuç: Yeni projelerde pandasql yerine DuckDB’yi seçmemiz lazım. Eski bir kod tabanına dokunuyorsak bilinmesi gerekiyor, hepsi o kadar.

Yol 4: DuckDB — Modern, Hızlı, Tam SQL

İşin yıldızı burada. DuckDB, “analitik için SQLite” olarak adlandırılan, sürecin içinde (in-process) çalışan bir analitik veritabanı [9][10]. Üç süper gücü var:

  1. Sıfır kopya entegrasyon (zero-copy integration): DuckDB, pandas DataFrame’i Arrow bellek formatı (memory format) üzerinden doğrudan okur — kopyalama, serileştirme (serialization), tip dönüşümü yok [11].
  2. Sütun temelli vektörlü motor (columnar vectorized engine): Veriyi satır satır değil, bloklar (batches) halinde işler ve CPU çekirdeklerini paralel kullanır [12].
  3. Tam SQL: Pencere fonksiyonları (window functions), CTE’ler, karmaşık join’ler, alt sorgular — hepsi var.

Kurulum

pip install duckdb pandas pyarrow

Tek satır, harici sunucu (external server) gerekmiyor [13].

En Basit Örnek

import duckdb
import pandas as pd

df = pd.DataFrame({
    "product": ["Laptop", "Klavye", "Mouse", "Monitör", "Tablet"],
    "price":   [25000, 850, 320, 7400, 12500],
    "stock":   [12, 45, 120, 8, 30],
    "category": ["electronics", "accessories", "accessories", "electronics", "electronics"],
})

# DataFrame'in adı doğrudan SQL içinde kullanılabiliyor!
result = duckdb.sql("SELECT * FROM df WHERE price > 1000").df()
print(result)

Burada sihir şu: df isimli yerel değişken (local variable) SQL içinde sanki bir tabloymuş gibi görünüyor. DuckDB bunu replacement scan denen mekanizmayla yapıyor — yani aslında DuckDB’de df diye bir tablo yok, ama sorgu çalıştığında pandas DataFrame otomatik tanınıp tablo olarak sunuluyor [14].

.df() çağrısı sonucu tekrar pandas DataFrame’e çeviriyor. .fetchdf() veya .to_df() da aynı işi yapıyor [15]. Eğer Polars kullanıyorsak .pl() ile Polars DataFrame’i de alabiliyoruz [11].

Filtreleme

result = duckdb.sql("""
    SELECT product, price, stock
    FROM df
    WHERE category = 'electronics' AND stock < 20
    ORDER BY price DESC
""").df()

Dönüştürme (Transformation)

result = duckdb.sql("""
    SELECT
        product,
        price,
        stock,
        price * stock AS total_value,
        CASE
            WHEN price > 10000 THEN 'premium'
            WHEN price > 1000  THEN 'mid-range'
            ELSE 'budget'
        END AS price_segment
    FROM df
""").df()
print(result)

CASE WHEN SQL’in en güzel ifadelerinden biri; pandas tarafında bunu yapmak için ya np.where zincirleri ya da pd.cut gerekiyordu. SQL’de tek blokta hallediyoruz [16].

Toplulaştırma (Aggregation)

result = duckdb.sql("""
    SELECT
        category,
        COUNT(*)        AS product_count,
        SUM(stock)      AS total_stock,
        AVG(price)      AS avg_price,
        MIN(price)      AS min_price,
        MAX(price)      AS max_price
    FROM df
    GROUP BY category
    ORDER BY avg_price DESC
""").df()
print(result)

Çıktı:

      category  product_count  total_stock  avg_price  min_price  max_price
0  electronics              3           50   14966.67       7400      25000
1  accessories              2          165     585.00        320        850

Pandas eşdeğeri (groupby().agg().reset_index() zinciri) de yapılabilir ama SQL hem daha kısa hem de gözle taranması daha kolay [16][17].

Birleştirme (Join)

İki DataFrame’imiz olsun:

orders = pd.DataFrame({
    "order_id": [1, 2, 3, 4, 5],
    "product": ["Laptop", "Mouse", "Laptop", "Tablet", "Klavye"],
    "quantity": [1, 3, 2, 1, 5],
})

# df ve orders'ı join edelim
result = duckdb.sql("""
    SELECT
        o.order_id,
        o.product,
        o.quantity,
        d.price,
        o.quantity * d.price AS line_total
    FROM orders o
    JOIN df d ON o.product = d.product
    ORDER BY line_total DESC
""").df()
print(result)

Birden fazla DataFrame’i aynı sorguda kullanabiliyoruz — hiçbirini önceden DuckDB’ye “kaydetmemiz” gerekmiyor [14][18].

Pencere Fonksiyonu (Window Function)

İşin nimet kısmı. Pandas’ta groupby + rank + transform zincirleri yerine:

result = duckdb.sql("""
    SELECT
        product,
        category,
        price,
        RANK() OVER (PARTITION BY category ORDER BY price DESC) AS price_rank,
        AVG(price) OVER (PARTITION BY category) AS category_avg
    FROM df
""").df()
print(result)

Her kategori içinde fiyat sıralaması ve kategori ortalaması — tek sorguda [19].

Kalıcı (Persistent) Bağlantı

duckdb.sql(...) çağrısı her seferinde geçici bir bellek bağlantısı (in-memory connection) yaratıyor. Birden fazla sorgu çalıştıracaksak bir bağlantıyı yeniden kullanmak daha verimli:

con = duckdb.connect()  # bellek içi, kalıcı bağlantı

con.register("products", df)
con.register("orders", orders)

# Artık SQL içinde bu isimleri kullanıyoruz
top_sellers = con.execute("""
    SELECT p.product, SUM(o.quantity) AS total_sold
    FROM products p
    JOIN orders o ON p.product = o.product
    GROUP BY p.product
    ORDER BY total_sold DESC
""").df()

con.close()

Diskte saklamak istersek duckdb.connect("mydb.duckdb") ile dosyaya yazan bir bağlantı da açılabiliyor [13][20].

Relational API: SQL Yazmadan SQL

DuckDB’nin az bilinen ama çok zarif bir özelliği daha var. Relational API ile pandas zinciri gibi metot zincirlemesi (method chaining) yazıp arkasında SQL üretebiliyoruz [21]:

import duckdb

con = duckdb.connect()
rel = con.from_df(df)

result = (rel
    .filter("category = 'electronics'")
    .project("product, price, price * 1.20 AS price_with_tax")
    .order("price_with_tax DESC")
    .limit(3)
    .df()
)

Bu, arka planda şuna eşdeğer:

SELECT product, price, price * 1.20 AS price_with_tax
FROM df
WHERE category = 'electronics'
ORDER BY price_with_tax DESC
LIMIT 3

Tembel değerlendirme (lazy evaluation) yapıldığı için DuckDB son .df() çağrısında tüm zinciri tek seferde optimize edip çalıştırıyor [21].

Performans: Sayılarla Gerçek Fark

DuckDB ekibinin lineitem tablosu üzerinde (TPC-H tarzı veri seti, 6 milyon satır) yaptığı kıyaslamada [8]:

  • Basit gruplu toplulaştırma (grouped aggregation) sorgusu — pandas: bir referans noktasında, DuckDB tek iş parçacığı (single-threaded) ile yaklaşık 2 kat daha hızlı, paralel modda ise çok daha hızlı.
  • pandasql aynı sorguda DuckDB’den ~1000 kat daha yavaş — kıyaslamayı bitirmek bile sorun oldu.

1 milyon satırlık bağımsız bir kıyaslamada (banka veri seti, filtre + toplulaştırma) DuckDB hem yürütme süresi (execution time) hem de bellek kullanımı (memory usage) açısından pandas’ı belirgin şekilde geçti; özellikle 1 GB üzeri verilerde fark daha da açılıyor [22]. Bir başka karşılaştırmada (anime puanları veri seti) pandas’ın 1 dakika 36 saniye aldığı işlemi DuckDB neredeyse anlık olarak bitirdi [23].

Tam Tur Pratik Örnek: Satış Analizi

Hadi gerçekçi bir senaryoyla birleştirelim. Üç DataFrame’imiz var: ürünler, siparişler, müşteriler.

import duckdb
import pandas as pd

products = pd.DataFrame({
    "product_id": [1, 2, 3, 4],
    "name": ["Laptop", "Klavye", "Mouse", "Monitör"],
    "category": ["electronics", "accessories", "accessories", "electronics"],
    "price": [25000.0, 850.0, 320.0, 7400.0],
})

customers = pd.DataFrame({
    "customer_id": [101, 102, 103, 104],
    "name": ["Ayşe", "Mehmet", "Fatma", "Can"],
    "city": ["İstanbul", "Ankara", "İzmir", "İstanbul"],
})

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005, 1006],
    "customer_id": [101, 102, 101, 103, 104, 101],
    "product_id": [1, 3, 2, 1, 4, 3],
    "quantity": [1, 2, 3, 1, 1, 5],
    "order_date": pd.to_datetime([
        "2026-01-15", "2026-01-20", "2026-02-03",
        "2026-02-10", "2026-02-15", "2026-03-01"
    ]),
})

con = duckdb.connect()

# Şehir başına toplam ciro ve top ürün
report = con.sql("""
    SELECT
        c.city,
        COUNT(DISTINCT o.order_id)            AS order_count,
        SUM(o.quantity * p.price)             AS total_revenue,
        ROUND(AVG(o.quantity * p.price), 2)   AS avg_order_value
    FROM orders o
    JOIN customers c ON o.customer_id = c.customer_id
    JOIN products  p ON o.product_id  = p.product_id
    WHERE o.order_date >= '2026-01-01'
    GROUP BY c.city
    ORDER BY total_revenue DESC
""").df()

print(report)

Bu sorgunun pandas eşdeğerini yazmaya kalksak (üç tane merge, sonra groupby, sonra agg, sonra rename, sonra sort_values) sayfanın yarısı dolardı [16][17].

Hangisini Ne Zaman Kullanalım?

Kabaca bir rehber:

  • Sadece bir filtreleme yapacaksak, küçük-orta DataFrame: df.query() yeterli, ek bir paket bile gerekmiyor [3][4].
  • Sütun türetme veya aritmetik ifade: df.eval() hızlı ve okunaklı [5].
  • Toplulaştırma, join, window function, karmaşık SQL: DuckDB [12][16][20].
  • Veri MB’den GB’ye geçtiğinde: yine DuckDB; pandas tek iş parçacıklı ve bellekte tutuyor, DuckDB paralel ve dışa taşıyor (out-of-core) [12][24].
  • Eski projede pandasql varsa: zamanı varken DuckDB’ye taşıyalım, performans farkı acımasız [8].

DuckDB pandas’ı öldürmüyor — onu tamamlıyor. Satır bazlı Python mantığı, apply ile özel fonksiyon, görselleştirme, ML kütüphanelerine geçiş için pandas hâlâ rakipsiz [10][24]. SQL’in parladığı yerlerde (analitik sorgular, büyük join’ler) DuckDB’ye geçip sonucu pandas’a iade etmek en pragmatik akış oluyor.

Bonus: SQL Sonucu Yine pandas’a Dönerken Tip Kaybı Olmuyor

Önceki yazımızda ADBC’den bahsederken vurguladığımız tip korunması (type preservation) konusu DuckDB’de de geçerli. DuckDB içte Arrow bellek formatı (memory format) kullandığı için, sorgu sonucunu .df() ile aldığımızda tipler temiz geliyor — özellikle dtype_backend="pyarrow" modlu pandas iş akışlarında round-trip neredeyse maliyetsiz [11][24].

con.execute("SELECT * FROM df").fetch_df(date_as_object=False)

veya doğrudan Arrow tablosu alıp pandas’a verme:

arrow_tbl = con.execute("SELECT * FROM df").fetch_arrow_table()
df_back = arrow_tbl.to_pandas(types_mapper=pd.ArrowDtype)

Sonuç

“Pandas DataFrame’lerini SQL ile sorgulayabilir miyim?” sorusunun cevabı: Evet, üstelik birkaç farklı yolla. Bugün için modern cevap DuckDB; hem sözdizimi (syntax) açısından tam SQL, hem performans olarak pandas’ı geride bırakacak kadar hızlı, hem de kurulumu tek satır. df.query() ve df.eval() yerel hız oyuncuları olarak küçük filtreler için cebimizde duruyor, pandasql ise nostaljik bir köşede.

Bir sonraki ETL akışımda büyük bir gruplu toplulaştırma yazacaksam doğrudan duckdb.sql("...").df() ile başlardım. Hem kod kısalıyor, hem makinem daha az terliyor, hem de SQL bilen takım arkadaşlarımla aynı dili konuşuyoruz. Üç kuş, tek taş.

Bir sonraki yazıda büyük olasılıkla DuckDB’yi doğrudan Parquet ve CSV dosyaları üzerinde nasıl kullanacağımıza bakacağız; takipte kalın!


Kaynaklar

[1] pandas.DataFrame.query — pandas documentation: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html

[2] Queiroz, F. — Pandas Query Examples: SQL-like queries in dataframes: https://queirozf.com/entries/pandas-query-examples-sql-like-syntax-queries-in-dataframes

[3] Kyriakidis, G. — How to Filter Pandas DataFrames Using ‘in’ and ‘not in’, Towards Data Science: https://towardsdatascience.com/pandas-in-notin-ff2415f1e3e1/

[4] SparkCodeHub — Mastering the Query Method in Pandas: https://www.sparkcodehub.com/pandas/data-manipulation/query-guide

[5] VanderPlas, J. — High-Performance Pandas: eval() and query(), Python Data Science Handbook: https://jakevdp.github.io/PythonDataScienceHandbook/03.12-performance-eval-and-query.html

[6] Oguntimehin, K. — Get More Efficient in your Data Analysis with Pandas query() and eval() Methods, Towards Data Science: https://towardsdatascience.com/get-more-efficient-in-your-data-analysis-with-pandas-query-and-eval-methods-3646317e591f/

[7] pandasql — PyPI: https://pypi.org/project/pandasql3

[8] Mühleisen, H. & Raasveldt, M. — Efficient SQL on Pandas with DuckDB, DuckDB Blog: https://duckdb.org/2021/05/14/sql-on-pandas

[9] DigitalOcean — How DuckDB Complements Pandas for Large-Scale Analytics: https://www.digitalocean.com/community/tutorials/duckdb-complements-pandas-for-large-scale-analytics

[10] Bantra, A. — Using DuckDB in Python: A Comprehensive Guide, Medium: https://medium.com/@anshubantra/using-duckdb-in-python-a-comprehensive-guide-d14bc0b06546

[11] MotherDuck — DuckDB Python Quickstart (Part 2): Pandas, Arrow, Polars & Python UDFs: https://motherduck.com/learn/duckdb-python-quickstart-part2/

[12] DZone — DuckDB for Python Developers: https://dzone.com/articles/duckdb-for-python-developers

[13] BetterStack — DuckDB for Python: A beginner’s guide: https://betterstack.com/community/guides/scaling-python/duckdb-python/

[14] DuckDB Documentation — SQL on Pandas: https://duckdb.org/docs/current/guides/python/sql_on_pandas

[15] Bala Priya, C. — How to Query Pandas DataFrames with DuckDB, Statology: https://www.statology.org/how-to-query-pandas-dataframes-with-duckdb/

[16] Rosidi, N. — 7 DuckDB SQL Queries That Save You Hours of Pandas Work, KDnuggets: https://www.kdnuggets.com/7-duckdb-sql-queries-that-save-you-hours-of-pandas-work

[17] DEV Community — DuckDB vs Pandas – Exploring DuckDB’s capabilities: https://dev.to/nageen20/duckdb-vs-pandas-exploring-duckdbs-capabilities-6kf

[18] MotherDuck — DuckDB vs Pandas vs Polars for Python Developers: https://motherduck.com/blog/duckdb-versus-pandas-versus-polars/

[19] DuckDB Documentation — Window Functions: https://duckdb.org/docs/sql/functions/window_functions

[20] DuckDB Documentation — Python API: https://duckdb.org/docs/api/python/overview

[21] DuckDB Documentation — Relational API on Pandas: https://duckdb.org/docs/current/guides/python/relational_api_pandas

[22] KDnuggets — We Benchmarked DuckDB, SQLite, and Pandas on 1M Rows: https://www.kdnuggets.com/we-benchmarked-duckdb-sqlite-and-pandas-on-1m-rows-heres-what-happened

[23] Hash Block — DuckDB vs Spark vs Pandas: The Benchmark Truth, Medium: https://medium.com/@connect.hashblock/duckdb-vs-spark-vs-pandas-the-benchmark-truth-6a9a184bf34a

[24] Codecentric — DuckDB vs DataFrame libraries benchmark: https://www.codecentric.de/en/knowledge-hub/blog/duckdb-vs-dataframe-libraries

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