BigQuery UDF vs TVF

“그 시점엔 어떤 값이었는지”를 찾는 조인을 쿼리마다 손으로 다시 쓰다 보니 세부 조건 하나를 빠뜨리는 실수가 반복됐어요. 이 조인을 함수 하나로 감쌌더니 데이터 몇 개로 시험할 땐 멀쩡했는데 100만 행이 넘는 전체 데이터에 적용하자 눈에 띄게 느려졌습니다.

원인은 함수를 만드는 방식에 있었어요. BigQuery 함수에는 값 하나만 돌려주는 종류와 표 전체를 돌려주는 종류가 따로 있는데, 저는 이 둘을 구분하지 않고 썼습니다.

첫 시도: 값 하나만 돌려주는 함수 (UDF)

BigQuery에서 흔히 만드는 함수(UDF, User-Defined Function)는 값을 하나만 돌려줘요. 유저 한 명과 날짜 하나를 넣으면 “그날 그 사람의 상태” 하나를 돌려주는 함수를 떠올리면 됩니다. 첫 시도에서는 이 함수 안에 조인 로직을 통째로 넣었어요.

CREATE OR REPLACE FUNCTION mart.get_status(p_user_id STRING, p_target_date DATE)
RETURNS STRING
AS ((
  SELECT status
  FROM `상태_이력_테이블`
  WHERE user_id = p_user_id
    AND p_target_date >= valid_from
    AND p_target_date < COALESCE(valid_to, DATE '9999-12-31')
));

(코드를 몰라도 괜찮아요. 유저 한 명과 날짜 하나를 넣으면 상태값 하나가 나오는 함수라는 점만 보면 됩니다.) 문법 오류도 없었고 유저 한두 명으로 테스트했을 땐 결과도 정확했습니다.

왜 느려졌나: 함수가 행마다 처음부터 다시 실행되는 구조였다

문제는 이 함수를 전체 유저 목록에 하나씩 적용하면서 생겼습니다.

SELECT
  a.user_id,
  a.target_date,
  mart.get_status(a.user_id, a.target_date) AS status
FROM 분석_대상_테이블 a

이렇게 쓰면 함수 안의 조회 로직이 분석_대상_테이블의 행마다 처음부터 다시 실행돼요. 유저 몇백 명일 땐 체감되지 않던 차이가 100만 행을 넘기자 눈에 띄게 벌어졌습니다. 저희 팀 마트 테이블은 크게는 몇천만 행까지 가요.

일반 JOIN은 두 명단을 나란히 펼쳐 놓고 한 번에 맞춰 보는 방식이에요. 제 함수는 명단을 한 줄 볼 때마다 다른 서랍을 처음부터 다시 뒤지는 방식이었습니다. 행이 많아질수록 이 차이가 그대로 실행 시간으로 쌓여요.

해결책: 표 전체를 돌려주는 함수 (TVF)

질문을 바꿨어요. 유저 한 명씩 “이 사람 상태가 뭐예요?”라고 묻는 대신 “이 날짜 기준 전체 유저 상태표를 통째로 주세요”라고 묻는 함수를 새로 만들었습니다. BigQuery에서는 값 하나가 아닌 테이블 자체를 돌려주는 함수를 TVF(Table Function)라고 불러요.

CREATE OR REPLACE TABLE FUNCTION mart.status_as_of(p_target_date DATE)
AS (
  SELECT
    user_id,
    status,
    valid_from,
    valid_to
  FROM `상태_이력_테이블`
  WHERE p_target_date >= valid_from
    AND p_target_date < COALESCE(valid_to, DATE '9999-12-31')
);

쓸 때는 일반 JOIN처럼 씁니다.

SELECT
  a.user_id,
  s.status
FROM 분석_대상_테이블 a
JOIN mart.status_as_of(DATE '2026-06-01') s
  ON a.user_id = s.user_id

status_as_of(2026-06-01)가 그 날짜 기준 전체 유저 상태표를 한 번에 돌려주면 그 표를 분석_대상_테이블에 JOIN으로 붙여요. 함수는 한 번만 호출되고 나머지는 평범한 JOIN이라 행마다 반복 실행되는 문제가 사라집니다.

함수로 감싸니 조인 조건의 세부 규칙도 함수 안 한 곳에만 두면 됐어요. “언제까지가 유효한 값인지” 계산하는 방식 같은 규칙이에요. 다른 PM이나 마케터는 이 규칙을 몰라도 날짜만 넣으면 항상 정확한 결과를 받습니다.

아직 안 풀린 부분: 유저마다 기준 날짜가 다를 때

TVF는 한 시점 기준의 전체 스냅샷이 필요할 때 잘 맞아요. 날짜별로 반복 호출하는 재계산 작업이 대표적입니다. 한 쿼리 안에서 유저마다 기준 날짜가 다르면 이 방식 하나로는 풀리지 않아요. 유저마다 본인 가입일 기준으로 계산해야 하는 경우가 그렇습니다. 이럴 땐 날짜별로 나눠 따로 호출하거나 처음 방식대로 조인 조건을 직접 썼어요. 매끄럽게 푸는 방법은 아직 찾는 중입니다.

함수로 감쌀 때 지키는 기준

  • 값 하나만 필요하고 호출 빈도가 적다면 UDF로도 충분해요. 대량 데이터의 전체 목록에 하나씩 적용한다면 TVF를 먼저 검토합니다.
  • 어느 쪽이든 함수 안에서 “오늘 날짜”나 “지금 시각”을 직접 참조하지 않게 만들어요. 기준 날짜를 항상 함수 밖에서 넣어 줘야 같은 날짜로 다시 물었을 때 같은 답이 나옵니다.
  • 함수로 감싸는 목적은 같은 로직을 여기저기 복붙하지 않는 데 있어요. 한 곳에서만 쓰는 조인이라면 굳이 함수로 만들지 않습니다.

공유하기