Requirements & constraints:- Join 100M-row feature table (F) with 1M-row training keys (T) by key, memory-limited.- Need correctness, reasonable speed, and reproducibility.High-level strategies (when to use which)- Partitioned (sharded) joins by key: safest, simple, parallelizable. Split both datasets into N shards by hash(key) so each shard fits memory; then join shard-by-shard and stream out results.- Hash-based streaming join: load smaller table (T or a subset) into an in-memory hash map and stream through F; good when smaller side fits memory.- Bloom-filter pre-filtering: build a Bloom filter from T to cheaply filter F; reduces IO/compute before heavier joins.- On-disk keyed stores: use LMDB/RocksDB/SQLite keyed DB for random lookups when lookup pattern is sparse and repeated.- Push joins into DB/warehouse: if F in a scalable DB (BigQuery, Redshift, Snowflake) push join there to leverage distributed resources and avoid transfer; use when cost/latency trade-off acceptable.Partitioned (sharded) approach — why and how- Hash partitioning ensures that all rows with same key land in same shard; shards can be sized to fit memory.- Workflow: (1) choose N based on memory/row-size; (2) stream-partition both datasets into N files; (3) for i in 0..N-1 load shard_i of T and F, do in-memory join, write output; (4) optionally compress/merge outputs.- Benefits: simple, reproducible, easily parallelizable, works with arbitrarily large datasets.Code sketch (partition, join, reduce memory):python
import pandas as pd
import hashlib
import os
import pyarrow.parquet as pq
def shard_id(key, N):
# stable hash to bucket
return int(hashlib.md5(str(key).encode()).hexdigest(), 16) % N
def partition_to_parquet(input_path, key_col, out_dir, N, chunksize=100_000):
os.makedirs(out_dir, exist_ok=True)
writers = {}
for i in range(N):
writers[i] = None
for chunk in pd.read_csv(input_path, chunksize=chunksize):
chunk['_shard'] = chunk[key_col].apply(lambda k: shard_id(k, N))
for sid, sub in chunk.groupby('_shard'):
path = os.path.join(out_dir, f'shard-{sid}.parquet')
if not os.path.exists(path):
sub.drop(columns='_shard').to_parquet(path, index=False)
else:
# append via pyarrow
table = pq.read_table(path)
new = pa.Table.from_pandas(sub.drop(columns='_shard'))
pq.write_table(pa.concat_tables([table, new]), path)
def process_shards(feature_dir, train_dir, out_dir, key_col, N):
os.makedirs(out_dir, exist_ok=True)
for i in range(N):
fpath = os.path.join(feature_dir, f'shard-{i}.parquet')
tpath = os.path.join(train_dir, f'shard-{i}.parquet')
if not os.path.exists(tpath):
continue
# load smaller shard first (training shard typically smaller)
train = pd.read_parquet(tpath)
features = pd.read_parquet(fpath)
# in-memory join (inner join); choose merge strategy that avoids copies
joined = train.merge(features, on=key_col, how='left')
out_path = os.path.join(out_dir, f'joined-{i}.parquet')
joined.to_parquet(out_path, index=False)
# free memory
del train, features, joined
# Example usage:
# 1) partition both tables into N shards (choose N so each shard fits memory)
# partition_to_parquet('features.csv', 'id', 'feature_shards', N=128)
# partition_to_parquet('train.csv', 'id', 'train_shards', N=128)
# 2) join per shard
# process_shards('feature_shards', 'train_shards', 'joined_shards', 'id', N=128)
Practical tips & trade-offs- Choose N so shard size < available memory / safety factor (e.g., 3x).- Use compression (parquet/snappy) to reduce disk; use fast columnar formats (parquet, feather).- Use Bloom filter: build from T and filter F in streaming pass to avoid writing many unnecessary feature rows.- Parallelism: run per-shard joins in parallel workers; watch I/O contention.- If feature table lives in data warehouse, push the join to the warehouse (cheaper and faster if compute available)—export only result if privacy/cost constraints allow.- For repeated lookups or feature store patterns, consider RocksDB/LMDB to serve features on-demand.- Monitor skew: heavy-key skew can break memory assumptions; handle by detecting hot keys and treating them separately (e.g., single-key streaming, sub-sharding).This approach is robust for ML pipelines: predictable memory use, easy to parallelize, and compatible with feature engineering steps per-shard.