PyCon JP 2026 · Hiroshima
chDB

Rediscovering
DataFrames

100× Analytics Without Leaving Pandas
Auxten Wang · ClickHouse · Creator of chDB@auxten
ABOUT · 自己紹介02 / 21
Auxten Wang

Auxten Wang

@auxten · auxten.com

Technical Director @ ClickHouse
Creator of chDB — acquired by ClickHouse in 2024
Builds embedded databases & data tooling for Python
Ex-Shopee · CovenantSQL · Baidu · Qihoo 360
chDB
ClickHouse
PANDAS · 03 / 21

Everyone's first data tool

import pandas as pd
df = pd.read_csv("sales.csv")
df[df.amount > 100] \
  .groupby("region").amount.sum()
In every tutorial, every notebook, every team
Muscle memory for millions of engineers
The DataFrame dialect LLMs write best
This API took 15 years
to learn everywhere.
The pandas API is an asset. The engine underneath is the problem.
THE CEILING · 限界04 / 21

Then your data grew.

Memory
Error
a 10 GB DataFrame chokes a 16 GB laptop
1 / 16
cores doing the work while fifteen watch
> query
reading from S3 takes longer than the analysis itself
Your code didn't hit the ceiling. The execution model did.
WHY · 原因05 / 21

The ceiling has three walls

EAGER

every step in the chain materializes a full intermediate copy

SINGLE-CORE

the GIL-era design: one thread computes, the rest idle

ALL-IN-RAM

the whole table decompresses into memory before row one is read
33 MB
Parquet on disk
1.5 GB
as a pandas DataFrame
×45 blow-up,
measured on my laptop.
Not fixable with another .apply() trick. It's the engine.
THE EXITS · 出口06 / 21

The usual exits all cost the same thing

Spark

a cluster to babysit, for one notebook

Polars

fast — but a new API for the whole team

DuckDB

great engine — but now you write SQL

Rewrite

months of migration, retraining, re-testing
Every exit asks you to leave the API. What if you didn't have to?
THE FLIP · 転換07 / 21

Change one line

- import pandas as pd + import chdb.datastore as pd
df  = pd.read_parquet("events.parquet")
top = (df[df.status == 200]
        .groupby("user_id").amount.sum()
        .sort_values(ascending=False).head(10))

Same code.
Different engine.
The rest of the file doesn't know. — one line.
CHDB08 / 21

chDB: a rocket engine on a bicycle

A rocket engine strapped to a bicycle
# pip install chdb
import chdb
chdb.query("SELECT count()
  FROM file('orders.parquet')")
# schema inferred · nothing loaded
The ClickHouse engine, in-process — like SQLite
No server, no config, no connection string
Columnar · vectorized · every core

Same engine that runs analytics at Anthropic, Cursor, Vercel, Cloudflare.

HOW · 仕組み09 / 21

Layered, not mixed —

sauce — your pandas calls

df[df.x > 1].groupby(…).sum() — untouched

noodles — the lazy op chain

every call recorded, nothing executed

cabbage — one SQL plan

the whole chain compiles to a single query

batter — the chDB engine

columnar · vectorized · all cores

teppan — your Python process

everything cooks in-process, no server
len() · print() · for — the flip: the whole stack executes at once.
BENCHMARK · 勝負10 / 21

Where the 100× lives

chDB
×1.35
DuckDB
×1.40
Polars
×9.99
pandas
×71.18
Worst single query: pandas 94.3 s vs chDB 0.037 s×2,550

ClickBench · DataFrame category · 43.77 GiB · same c6a.metal machine · relative runtime, lower is better

Same DataFrame workload. ×71 less waiting — before any tuning.
BENCHMARK · 勝負11 / 21

10M rows, 20 everyday ops — wins 14 / 20

Pandas vs DataStore benchmark across 20 DataFrame operations at 10M rows: DataStore wins 14, pandas wins 6
pandas keeps 6 —
and we'll tell you
which ones.
Complex chains win big. Tiny cached ops stay with pandas — honesty slide ahead.
REACH · 到達12 / 21

Your data lives on many islands

pd.read_parquet("https://…/house_0.parquet"# the lake pd.read_csv("crm_exports.tar :: users_*.csv"# archives pd.read_sql("SELECT …", "postgresql://prod")  # live DBs

measured this week, this laptop

2,772,030 rows on S3 — counted in 3.3 s, no download
parquet ⋈ CSVs-inside-a-tar ⋈ groupby — one chain
実測
70 table functions · 72 input formats — counted from the engine
Like the Miyajima torii: a gateway standing in the water. The data never moves.
PERFORMANCE · 性能13 / 21

It reads what the query needs — not the file

house_0.parquet · 33 MB · 2.77M rows · 14 columns    ▼ HTTP Range reads
type
price
date
town
street
…9 more columns
footer
fetched for avg(price) by type   ■ skipped   ■ footer = schema + stats, read first
3.3 s
len() on the remote file — footer + counts only, no download
2 / 14
columns fetched for the aggregation — ranged reads, no full scan
344 vs 975 MB
peak RAM, chDB streaming vs pandas on the same file
Ranged reads: you pay for the columns you touch — not the file.
DEMO · 実演14 / 21

Live: a 10M-row access log, migrated

import pandas as pd  # ← the only line we'll touch
logs = pd.read_parquet("access_log.parquet"# 10M rows
slow = logs[logs.url.str.contains("/search/")]
p99  = slow.latency_ms.quantile(0.99)
0.66 s
pandas, every run — filter is the hot path
367.8
the p99 latency it computes — remember this number
String filter +
quantile — pandas'
least favorite food.
Ordinary pipeline, ordinary laptop. Now change one line — live.
DEMO · 実演15 / 21

Same file. Same answer. One line later.

- import pandas as pd + import chdb.datastore as pd # …nothing else changes…
0.66 s
pandas
0.16 s
chdb.datastore — ×4.1
367.8
identical answer, checked every run
実測
×4.1 on a laptop, file in page cache. The ×71 arrives as the data grows — same code.
Same numbers out, less waiting. That's the entire migration.
VECTOR · ベクトル16 / 21

Vector search is a column, not a service

from datastore import F
docs = pd.read_parquet("docs.parquet")  # emb: Array(Float32)
docs["dist"] = F.cosine_distance(
    docs.emb, F.array(*query_vec))
docs.sort_values("dist").head(3)

top-3, measured

api rate limits  0.000 ← the query itself
refund policy    0.946
shipping delay   1.119
実測
No second database to run
Embeddings live next to the rows they describe
Schema gotcha: needs non-nullable Float32 arrays
No vector DB. An Array column and a sort.
TIME · 時系列17 / 21

Time series, the calls you already know

d = pd.read_parquet("sensor.parquet")
daily = d.resample("D", on="ts").value.mean()
d["ma24"] = d.value.rolling(24).mean()

daily means, measured

2026-01-01   99.81
2026-01-02  101.78
2026-01-03   99.08  …
実測
resample · rolling · shift · diff
Backed by ClickHouse's date & window functions
Windows run columnar, on all cores
The ops that hurt most
at 3am on-call — faster.
Same idioms. The clock runs columnar now.
HONESTY · 正直18 / 21

— show the cracks in gold

Small cached data: pandas wins

value_counts on a cached file: pandas 0.13 s, DataStore 1.61 s. Below ~1M rows, don't bother switching.

API coverage is not 100%

during prep, str.extract inside a groupby came back empty — mirror-test your pipeline, don't assume.

DuckDB is also excellent

×1.40 vs our ×1.35 — a coin flip. chDB is the ClickHouse flavor: more formats, and a straight path to a shared cluster.
Tools you can trust tell you when not to use them.
MIGRATE · 移行19 / 21

Migrate like a daruma: one eye first

01

Swap at the I/O boundary. read_parquet / read_csv first — the cheapest, biggest wins.

02

Move the hot paths. The slow groupbys, the string filters — one column, one op at a time.

03

Mirror-test everything. Same input, both engines, assert equal — never trust, always verify.

04

Keep the escape hatch. .to_pandas() drops any result back — any op, any time.

Paint the second eye when the pipeline runs green.
CLOSE · まとめ20 / 21

Three takeaways

01

The pandas API is the asset. Fifteen years of muscle memory — keep it, swap the engine.

02

The ceiling is the execution model. Eager, single-core, all-in-RAM — not your code.

03

Migrate daruma-style. One line, one column at a time — mirror-tested, escape hatch open.

pandas is the language. chDB is the engine. You never had to leave.

Thank you.

pip install chdb
github.com/chdb-io/chdb
clickhouse.com/docs/chdb
auxten.com — slides & verified demos
Q&A
Auxten Wang · ClickHouse · Creator of chDB@auxten