run_query 매크로
run_query 매크로
run_query 매크로는 쿼리를 실행하고 결과를 가져오는 편리한 방법을 제공해요. 이것은 더 유연하지만 더 복잡하게 쓰기 어려운 statement block을 감싼 래퍼(wrapper)예요. run_query가 처음이라면 using Jinja 가이드에서 run_query 매크로 결과를 다루는 예시(8단계)를 참고해요.
출처: 문서
본문
경고해요 —
run_query는 dbt가 라이브 연결(connection)으로 프로젝트를 컴파일할 때마다(기본적으로dbt compile와dbt docs generate포함) 웨어하우스에 SQL을 실행해요. 컴파일은 Jinja와 매크로를 해석하므로 이는 예상된 동작이에요.run_query안에 DML이나 다른 부작용(side-effecting) 문을 넣으면, 예를 들어flags.WHICH로 범위를 제한하지 않는 한 그 워크플로에서도 실행돼요.
인자(Args)
sql: 실행할 SQL 쿼리
쿼리 결과를 담은 Table 객체를 반환해요. 지정된 쿼리가 결과를 반환하지 않으면(예: DDL, DML, 또는 유지보수 쿼리) 반환 값은 none이 돼요.
(앱: dbt v2.0 이상)
dbt v2 타입 검사
dbt v2는 더 엄격한 null 검사로 결과 셋을 처리해요. 이 때문에 DDL이나 유지보수 작업(예: OPTIMIZE, VACUUM)에 run_query를 쓰면 실패할 수 있어요. 결과 셋이 non-nullable로 선언된 컬럼에서 null 값을 반환하면 v2는 실패해요. 반면 dbt v1은 이를 조용히 무시했어요.
결과 셋이 필요 없는 "fire and forget" 작업이라면, 이 Databricks 예시처럼 fetch_result=False를 가진 statement block을 대신 사용해요:
{% macro run_optimize(table, zorder_fields) %}
{% set zorder_str = zorder_fields | join(', ') %}
{% set query %}
OPTIMIZE {{ table }}
{% if zorder_str | length > 0 %}
ZORDER BY ({{ zorder_str }})
{% endif %}
{% endset %}
{% call statement('optimize', fetch_result=False) %}
{{ query }}
{% endcall %}
{% endmacro %}
run_query를 처음 쓰시나요? — using Jinja 가이드에서run_query매크로 결과를 다루는 예시를 확인해보세요!
참고: run_query 매크로는 트랜잭션을 자동으로 시작하지 않아요. 쿼리를 트랜잭션 안에서 실행하려면 적절히 begin과 commit 문을 사용해요.
예시
models/my_model.sql
{% if execute %}
{% set results = run_query('select 1 as id') %}
{% else %}
{% set results = none %}
{% endif %}
{% if results is not none %}
{{ log(results.print_table(), info=True) }}
{% endif %}
{# do something with `results` here... #}
macros/run_grants.sql
{% macro run_vacuum(table) %}
{% set query %}
vacuum table {{ table }}
{% endset %}
{% do run_query(query) %}
{% endmacro %}
컬럼 값을 반환하려고 run_query를 쓴다면, dbt-utils 패키지의 get_column_values 매크로를 확인해보세요. 다음은 사용 예시예요:
models/my_model.sql
{% set payment_methods_query %}
select distinct payment_method from app_data.payments
order by 1
{% endset %}
{% set results = run_query(payment_methods_query) %}
{% if execute %}
{# Return the first column #}
{% set results_list = results.columns[0].values() %}
{% else %}
{% set results_list = [] %}
{% endif %}
select
order_id,
{% for payment_method in results_list %}
sum(case when payment_method = '{{ payment_method }}' then amount end) as {{ payment_method }}_amount,
{% endfor %}
sum(amount) as total_amount
from {{ ref('raw_payments') }}
group by 1
run_query를 사용해 select 문이 아닌 SQL 쿼리도 수행할 수 있어요.
macros/run_vacuum.sql
{% macro run_vacuum(table) %}
{% set query %}
vacuum table {{ table }}
{% endset %}
{% do run_query(query) %}
{% endmacro %}
length 필터를 사용해 run_query가 행을 반환했는지 확인할 수 있어요. 파싱 중(execute가 False일 때) run_query가 실행되지 않도록 로직을 if execute 블록으로 감싸세요.
{% if execute %}
{% set results = run_query(payment_methods_query) %}
{% if results|length > 0 %}
-- do something with `results` here...
{% else %}
-- do fallback here...
{% endif %}
{% endif %}
컴파일과 dbt docs generate 중 run_query가 동작하는 방식
dbt가 라이브 웨어하우스 연결로 모델과 다른 리소스를 컴파일할 때, Jinja를 평가하고 프로젝트가 도달하는 run_query() 호출을 실행해요. 이는 정상적인 동작이에요. 내성적(introspective) 매크로는 다른 컴파일 단계와 마찬가지로 웨어하우스가 필요하거든요. 컴파일은 dbt run과 dbt build 중에 일어나고, dbt compile, dbt docs generate(컴파일이 실행될 때), 그리고 프로젝트를 컴파일하는 다른 명령어에서도 일어나요. 단순히 dbt가 테이블을 "빌드"하는 단계에서만 일어나는 게 아니에요.
dbt docs generate는 기본적으로 프로젝트를 컴파일해요(--no-compile을 전달하지 않는다면). 그래서 run_query()는 특정 dbt run 선택에 포함되지 않은 리소스를 포함해 다른 컴파일 워크플로와 같은 규칙으로 문서 생성 중에 실행돼요.
컴파일 중에는 execute가 True
execute 컨텍스트 변수는 모델을 dbt run 중 materialize할 때뿐만 아니라, dbt가 연결로 컴파일할 때마다 True예요. {% if execute %} 같은 가드는 여전히 run_query()가 dbt docs generate와 dbt compile 중에 실행되도록 허용해요. 왜냐하면 그 명령어들이 프로젝트를 컴파일하기 때문이에요. {% if execute and is_incremental() %} 같은 패턴은 incremental 모델 SQL이 언제 실행될지는 바꾸지만, 컴파일 자체를 끄지는 않아요. 그래서 Jinja가 그 경로들에서 실행하지 않는 한, docs나 compile 중 run_query()를 자체적으로 건너뛰지는 않아요.
DML이나 다른 부작용 SQL이 특정 dbt 명령어에서만 실행되게 하려면, 예를 들어 flags.WHICH 같은 조건을 하나 더 추가해요.
flags.WHICH로 부작용 SQL 범위 제한하기
execute를 flags.WHICH와 결합해, 활성 명령어가 원하는 것(run, build 등)일 때만 DML이 실행되고, docs나 compile 등 부작용이 예상치 못한 명령어에서 dbt가 컴파일할 때는 실행되지 않게 해요. 전체 명령어 값 목록은 flags.WHICH 표를 참고해요.
{% if execute and flags.WHICH in ['run', 'build'] %}
{% do run_query('delete from my_scratch_table where session_id = ...') %}
{% endif %}
매크로가 실행되어야 하는 곳에 맞게 명령어 목록을 조정해요.
더 알아보기 (Learn more)
run_query는 쿼리 결과를 테이블로 가져오지만, DDL/유지보수 작업은 statement block +fetch_result=False가 권장돼요.- 관련 개념: statement blocks, execute.