When two large DataFrames don't fit comfortably in memory, there are several practical strategies. I'll outline each, trade-offs, and short Python sketches.1) Index-based joins (set_index + join)- Approach: set user_id as index on both; use join which can be faster if indices are sorted.- Trade-offs: requires both frames to fit (or one to fit while the other is chunked); set_index can copy data.- Sketch:python
# assumes smallish left, large right on disk-backed format
left = left.set_index('user_id')
right = right.set_index('user_id')
res = left.join(right, how='inner')
2) Categorical codes to reduce memory- Approach: convert user_id to pandas.Categorical to replace long strings with ints; helps if cardinality << rows.- Trade-offs: cost to factorize; categories stored in memory; not helpful if cardinality is huge.python
cats, codes = pd.factorize(df['user_id'])
df['user_id_code'] = codes
# join on user_id_code
3) Chunked joins (streaming/hash-join per chunk)- Approach: iterate one DataFrame in chunks, load smaller partition of the other (or pre-partition both into files by hash of user_id), join chunk-by-chunk, write out.- Trade-offs: I/O heavy but memory bound; easier to implement; deterministic.python
def join_chunks(left_iter, right_path):
for left_chunk in pd.read_csv(left_path, chunksize=1_000_000):
# load right filtered by keys in chunk (if indexed or pre-partitioned)
keys = left_chunk['user_id'].unique()
right_subset = load_right_for_keys(keys) # from a DB or pre-partitioned files
out = left_chunk.merge(right_subset, on='user_id', how='inner')
out.to_parquet('out.parquet', append=True)
4) Dask / Spark (distributed)- Approach: use Dask DataFrame or PySpark for out-of-core and distributed joins. Both handle partitioning and shuffles.- Trade-offs: setup overhead, cluster resources, shuffle cost for large joins; Spark is more mature for big joins.python
import dask.dataframe as dd
L = dd.read_parquet('left/*.parquet')
R = dd.read_parquet('right/*.parquet')
res = L.merge(R, on='user_id', how='inner') # triggers shuffle
res.to_parquet('joined/')
5) Disk-based DB (Postgres / DuckDB)- Approach: load tables into a disk-backed DB and use SQL joins; DuckDB is excellent in-process and handles large data efficiently.- Trade-offs: load time; DuckDB offers fast vectorized queries without cluster overhead.python
import duckdb
duckdb.query("CREATE TABLE left AS SELECT * FROM 'left.parquet'")
duckdb.query("CREATE TABLE right AS SELECT * FROM 'right.parquet'")
duckdb.query("CREATE TABLE joined AS SELECT * FROM left JOIN right USING(user_id)")
Guidance to choose:- If cardinality small → factorize then join in pandas.- If one table fits → set_index on that table and chunk the other.- If both huge and you need speed/scale → Dask/Spark or DuckDB (DuckDB often simplest and fastest for single-node).- If available cluster + repeated queries → Spark.Always profile: measure memory, IO, and shuffle. Pre-partitioning by hash(user_id) dramatically reduces memory pressure and repeated full scans.