
‘pandas merge 메모리’라는 키워드로 검색해서 들어오셨다면, 이미 merge() 한 줄 실행했다가 커널이 죽거나 스왑이 도는 상황을 겪으셨을 가능성이 큽니다. 결론부터 말씀드리면, 원인은 대개 두 가지입니다. 조인 방식(how, 조인 키 선택)이 결과 행 수를 불필요하게 부풀리고 있거나, 조인 키로 쓰는 문자열 컬럼이 object dtype이라 메모리를 과하게 먹고 있는 경우입니다. 이 글은 pandas 2.2 버전을 기준으로, 조인 전략을 손보는 방법과 category dtype으로 바꿔서 메모리를 줄이는 구체적인 절차를 다룹니다.
조인 키를 합칠 때 메모리가 두세 배로 뛰는 이유
pd.merge()는 기본적으로 왼쪽·오른쪽 DataFrame을 그대로 두고 새 결과 객체를 만듭니다. 즉 원본 두 개 메모리에 결과 하나가 더해지는 구조라, 순간 최대 메모리 사용량은 최소 세 프레임 분량이라고 보시면 됩니다.
여기에 조인 키가 중복되는 다대다(many-to-many) 관계라면 상황이 더 나빠집니다. 왼쪽에 같은 키가 3번, 오른쪽에 5번 있으면 결과 행은 15개로 늘어나는 식으로, 카티션 곱에 가까운 행 폭발이 일어날 수 있습니다. 매출 로그처럼 키가 자주 중복되는 데이터를 다룰 때 흔히 겪는 패턴입니다.
먼저 확인할 것은 조인 전 두 프레임의 키 중복도입니다.
print(left['key'].duplicated().sum())
print(right['key'].duplicated().sum())
이 값이 크다면 merge 전에 groupby()로 미리 집계해서 키당 한 행으로 줄이는 편이, dtype을 손대는 것보다 먼저 해야 할 작업입니다.
how=’inner’와 how=’outer’, 메모리 차이는 얼마나 날까요?
how 값에 따라 결과 행 수가 크게 달라집니다. inner는 양쪽에 다 있는 키만 남기니 결과가 가장 작고, outer는 양쪽 키를 합집합으로 유지하면서 매칭 안 되는 자리를 NaN으로 채우니 결과가 가장 커집니다. left, right는 그 중간입니다.

실무에서 자주 하는 실수는 필요한 게 inner인데 습관적으로 left를 써서 결과에 불필요한 NaN 행까지 끌고 가는 경우입니다. 분석 목적이 “두 테이블 모두에 존재하는 데이터만 보겠다”라면 how='inner'로 명시하는 것만으로도 결과 크기를 줄일 수 있습니다.
또 하나, merge 전에 정말 필요한 컬럼만 남기는 것도 효과가 큽니다.
result = pd.merge(
left[['key', 'amount']],
right[['key', 'category_code']],
on='key',
how='inner'
)
전체 컬럼을 다 들고 merge한 뒤 나중에 drop()하는 습관이 있다면, 순서를 바꿔서 merge 전에 컬럼을 줄이는 것만으로 피크 메모리가 눈에 띄게 낮아집니다. 조인 대상 컬럼 수를 줄이는 것이 조인 알고리즘 자체를 바꾸는 것보다 손쉬운 최적화입니다.
문자열 키는 category dtype으로 바꿔서 merge해 보세요
조인 키가 ‘서울’, ‘부산’ 같은 저(低)카디널리티 문자열이라면 object dtype 대신 category dtype으로 바꾸는 것이 효과적입니다. object dtype 컬럼은 각 행마다 파이썬 문자열 객체를 따로 참조하기 때문에, 값이 반복되더라도 행 수만큼 메모리를 씁니다. category dtype은 실제 값을 정수 코드 배열로 저장하고, 고유 값 목록(categories)만 따로 한 번 저장하는 구조라 저카디널리티 컬럼에서 절약 폭이 큽니다.
import pandas as pd
df = pd.DataFrame({
'city': ['서울', '부산', '대구', '인천', '광주'] * 200_000
})
print(df['city'].memory_usage(deep=True))
df['city'] = df['city'].astype('category')
print(df['city'].memory_usage(deep=True))
행이 100만 개, 고유 값이 5개인 이 예시라면 object 버전은 문자열 객체 하나당 대략 50~60바이트씩 잡아먹어 수십 MB 수준이 되고, category 버전은 코드 배열(값 종류가 5개라 int8, 1바이트)이 주된 비용이라 1~2MB 안팎까지 줄어드는 것을 확인하실 수 있습니다. 정확한 바이트 수는 파이썬 빌드와 문자열 길이에 따라 달라지니, 직접 memory_usage(deep=True)로 측정해 보시는 게 정확합니다. dtype 관련 공식 설명은 pandas 공식 문서의 categorical 가이드에서 확인하실 수 있습니다.
category로 바꿔도 메모리가 안 줄어드는 경우가 있습니다
category dtype이 항상 이득인 것은 아닙니다. 먼저 고유 값 비율이 높은 컬럼(예: 주문번호, UUID처럼 거의 다 다른 값)에는 오히려 손해입니다. 코드 배열에 더해 카테고리 목록 자체가 원본 데이터만큼 커지기 때문에, 메모리 절약 효과가 없거나 오히려 오버헤드만 늘어납니다.

두 번째로, 조인 대상 좌우 프레임의 category 컬럼이 서로 다른 categories를 가지고 있으면 pandas가 병합 시점에 내부적으로 공통 dtype(대개 object)으로 되돌립니다. 즉 category로 바꿔둔 의미가 조인 순간에 사라지는 셈입니다.
left['key'] = left['key'].astype('category')
right['key'] = right['key'].astype('category')
# 두 시리즈의 categories 구성이 다르면
# pandas가 자동으로 공통 dtype으로 되돌립니다
result = pd.merge(left, right, on='key')
이 문제를 피하려면 조인 전에 union_categoricals()로 두 컬럼의 카테고리를 통일하거나, astype(CategoricalDtype(categories=공통목록))으로 명시적으로 맞춰주는 절차가 필요합니다. 관계형 데이터의 조인 개념 자체가 궁금하시다면 위키백과의 조인(SQL) 문서도 참고하실 만합니다.
category로 바꿨는데 merge가 오히려 느려진 이유는 뭘까요?
앞서 설명한 categories 불일치가 가장 흔한 원인입니다. 좌우 category 컬럼의 categories 순서나 구성이 다르면 pandas가 내부에서 dtype을 정리하는 변환 비용이 추가로 붙어서, 차라리 처음부터 문자열로 merge하는 것보다 느려질 수 있습니다.
두 번째 원인은 데이터 자체가 크지 않은데 category 변환 비용만 든 경우입니다. 행이 몇만 건 수준으로 애초에 메모리 압박이 없던 데이터라면, dtype 변환에 드는 시간이 조인 시간 절약분보다 클 수 있습니다. category dtype은 ‘메모리가 실제로 부족하거나 저카디널리티 문자열 컬럼이 여러 개’일 때 쓰는 최적화이지, 모든 merge에 기본으로 적용할 규칙은 아닙니다.
그래도 메모리가 부족한 상황이 반복된다면 데이터를 통째로 메모리에 올리는 대신, pd.read_csv(chunksize=...)로 청크 단위로 읽어 작은 조회 테이블과 merge한 뒤 결과만 이어 붙이는 방식이나, dask.dataframe·duckdb.sql()처럼 메모리 밖(out-of-core) 처리를 지원하는 도구로 넘어가는 것도 방법입니다. merge 관련 파라미터의 전체 목록은 pandas 공식 API 문서에서 확인하실 수 있습니다.
큰 DataFrame은 합치기 전에 줄이는 순서가 먼저입니다
정리하면 순서는 이렇습니다. 먼저 how를 목적에 맞게 inner로 좁힐 수 있는지 보고, 필요한 컬럼만 남긴 뒤, 키 중복도를 확인해서 groupby로 미리 줄일 수 있는지 살핍니다. 그다음에야 저카디널리티 문자열 키를 category dtype으로 바꾸되, 좌우 categories를 통일하는 절차를 빠뜨리지 않아야 합니다.
지금 바로 해보실 일은 df.info(memory_usage='deep')로 현재 조인 키 컬럼들의 실제 메모리를 확인하는 것입니다. object dtype 컬럼 중 고유 값 비율이 낮은 게 있다면 그 컬럼부터 category로 바꿔보고, 변환 전후 memory_usage(deep=True) 값을 비교해 실제로 얼마나 줄어드는지 직접 확인해 보시길 권합니다.
