Skip to content

feat(athena): add build_strategy config for incremental and table materializations - #1830

Open
dtaniwaki wants to merge 10 commits into
dbt-labs:mainfrom
dtaniwaki:feat/athena-build-with-subquery
Open

feat(athena): add build_strategy config for incremental and table materializations#1830
dtaniwaki wants to merge 10 commits into
dbt-labs:mainfrom
dtaniwaki:feat/athena-build-with-subquery

Conversation

@dtaniwaki

@dtaniwaki dtaniwaki commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

resolves #1829
docs dbt-labs/docs.getdbt.com/#

Problem

Athena incremental models always stage data into a __dbt_tmp table via CTAS before applying MERGE or INSERT. The tmp table exists to support automatic batching when TOO_MANY_OPEN_PARTITIONS occurs, but many models never hit this limit. For these models, the tmp table is pure overhead — unnecessary S3 writes, doubled scan costs, and added latency.

Solution

Add build_strategy config option to control how a model's source SQL is materialized. The default tmp_table preserves today's behavior; subquery skips the full CTAS and uses the compiled SQL directly as a subquery.

  • Empty tmp table for schema comparison: CTAS ... WITH NO DATA (zero data scan) preserves the process_schema_changes flow, so on_schema_change is fully supported
  • Merge strategy: MERGE INTO target USING (subquery) AS src ON ...
  • Append strategy: INSERT INTO target SELECT ... FROM (subquery)
  • Insert overwrite strategy: subquery feeds the partition-overwrite INSERT directly
  • Table materialization: CREATE TABLE AS subquery skips the intermediate tmp table
  • Works with both Iceberg and Hive table types

Example configuration:

# dbt_project.yml or model config
models:
  my_project:
    +build_strategy: subquery
-- model file
{{ config(build_strategy='subquery') }}
select ...

Constraints:

  • subquery is incompatible with force_batch — batching requires physical tmp table partition metadata
  • subquery is not supported with Python models
  • With subquery, TOO_MANY_OPEN_PARTITIONS raises an error instead of auto-batching (clear message guides user to switch back to tmp_table)

Test infrastructure (Terraform)

The functional tests in tests/functional/adapter/test_build_strategy.py exercise every combination of build_strategy × incremental strategy × table type against a real Athena account. The Terraform below provisions the minimal AWS resources needed.

Terraform (click to expand)
terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  profile = "<your-aws-profile>"
  region  = "<your-region>"
}

locals {
  name = "<your-test-name>"
}

resource "aws_s3_bucket" "test" {
  bucket        = local.name
  force_destroy = true
}

resource "aws_s3_bucket_public_access_block" "test" {
  bucket                  = aws_s3_bucket.test.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_glue_catalog_database" "test" {
  name = replace(local.name, "-", "_")
}

resource "aws_athena_workgroup" "test" {
  name        = local.name
  description = "Functional test workgroup for dbt-athena build_strategy"

  configuration {
    enforce_workgroup_configuration = true

    result_configuration {
      output_location = "s3://${aws_s3_bucket.test.bucket}/output/"
    }
  }
}

output "s3_staging_dir" {
  value = "s3://${aws_s3_bucket.test.bucket}/staging/"
}

output "s3_tmp_table_dir" {
  value = "s3://${aws_s3_bucket.test.bucket}/tmp/"
}

output "database" {
  value = aws_glue_catalog_database.test.name
}

output "workgroup" {
  value = aws_athena_workgroup.test.name
}

Checklist

  • I have read the contributing guide and understand what's expected of me
  • I have run this code in development and it appears to resolve the stated issue
  • This PR includes tests, or tests are not required/relevant for this PR
  • This PR has no interface changes (e.g. macros, cli, logs, json artifacts, config files, adapter interface, etc) or this PR has already received feedback and approval from Product or DX

@cla-bot cla-bot Bot added the cla:yes The PR author has signed the CLA label Apr 6, 2026
@dtaniwaki
dtaniwaki force-pushed the feat/athena-build-with-subquery branch from d1d5a19 to 4be0baa Compare April 6, 2026 14:35
Signed-off-by: Daisuke Taniwaki <daisuketaniwaki@gmail.com>
@dtaniwaki
dtaniwaki force-pushed the feat/athena-build-with-subquery branch from 4be0baa to 5bd01f8 Compare April 6, 2026 14:42
@dtaniwaki
dtaniwaki marked this pull request as ready for review April 6, 2026 14:42
@dtaniwaki
dtaniwaki requested a review from a team as a code owner April 6, 2026 14:42
Copilot AI review requested due to automatic review settings April 6, 2026 14:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Athena adapter config (build_with_subquery) to avoid staging incremental model results into a physical __dbt_tmp CTAS table for append and Iceberg merge, reducing S3 write/read overhead and scan cost by using the compiled model SQL as a subquery instead.

Changes:

  • Introduces build_with_subquery adapter config and materialization logic to use USING ({{ compiled_code }}) / FROM ({{ compiled_code }}) while still creating an empty tmp table for schema comparison.
  • Adds guardrails/errors for unsupported combinations (Python models, force_batch, insert_overwrite) and for TOO_MANY_OPEN_PARTITIONS when subquery mode is enabled.
  • Adds unit + functional tests covering SQL generation and end-to-end behavior for merge/append and incompatibility with force_batch.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
dbt-athena/src/dbt/adapters/athena/impl.py Adds build_with_subquery to AthenaConfig and documents intended behavior.
dbt-athena/src/dbt/include/athena/macros/materializations/models/incremental/incremental.sql Reads build_with_subquery and wires subquery mode into append + Iceberg merge paths (empty CTAS for schema comparison).
dbt-athena/src/dbt/include/athena/macros/materializations/models/incremental/helpers.sql Extends incremental_insert to optionally insert from a subquery and error on open partitions in subquery mode.
dbt-athena/src/dbt/include/athena/macros/materializations/models/incremental/merge.sql Extends iceberg_merge to optionally USING (subquery) and error on open partitions in subquery mode.
dbt-athena/tests/unit/test_config.py Adds unit coverage for AthenaConfig default/override of build_with_subquery.
dbt-athena/tests/unit/test_build_with_subquery.py Adds unit tests validating generated SQL for insert/merge and open-partitions behavior.
dbt-athena/tests/functional/adapter/test_build_with_subquery.py Adds functional coverage for Iceberg merge/append and Hive append plus force_batch incompatibility.
dbt-athena/.changes/unreleased/Features-20260406-225458.yaml Adds changelog entry for the new feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +54 to +59
{% if force_batch %}
{% do exceptions.raise_compiler_error('build_with_subquery is incompatible with force_batch. Batching requires data in the tmp table.') %}
{% endif %}
{% if strategy == 'insert_overwrite' %}
{% do exceptions.raise_compiler_error('build_with_subquery is not supported with insert_overwrite strategy.') %}
{% endif %}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build_with_subquery validation block runs unconditionally, which will raise a compiler error even on --full-refresh runs (e.g., when build_with_subquery=True and force_batch=True). The new functional tests expect full-refresh to succeed and only the incremental run to fail, and it’s also reasonable for full-refresh to ignore this incremental-only optimization. Consider gating these checks to only apply on incremental runs (e.g., when existing_relation is not none and not should_full_refresh()), or update the tests/behavior consistently.

Suggested change
{% if force_batch %}
{% do exceptions.raise_compiler_error('build_with_subquery is incompatible with force_batch. Batching requires data in the tmp table.') %}
{% endif %}
{% if strategy == 'insert_overwrite' %}
{% do exceptions.raise_compiler_error('build_with_subquery is not supported with insert_overwrite strategy.') %}
{% endif %}
{% if existing_relation is not none and not should_full_refresh() %}
{% if force_batch %}
{% do exceptions.raise_compiler_error('build_with_subquery is incompatible with force_batch. Batching requires data in the tmp table.') %}
{% endif %}
{% if strategy == 'insert_overwrite' %}
{% do exceptions.raise_compiler_error('build_with_subquery is not supported with insert_overwrite strategy.') %}
{% endif %}
{% endif %}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there're pros and cons in ignoring the error in the full-refresh phase. The error will be eventually raised in the incremental phase, so we should catch it earlier.

Comment thread dbt-athena/src/dbt/adapters/athena/impl.py Outdated
dtaniwaki and others added 2 commits April 7, 2026 00:10

@iconara iconara left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see this being implemented, I think it will be appreciated.

{% if old_tmp_relation is not none %}
{% do drop_relation(old_tmp_relation) %}
{% endif %}
{%- set empty_sql = 'SELECT * FROM (' ~ compiled_code ~ ') _dbt_sbq WITH NO DATA' -%}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any chance that compiled_code could end with a comment? I assume that this could happen if for example the model's last line has a comment. In that case a newline is needed before the closing paragraph to avoid the rest of the statement ending up inside of the comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed it 👍
f95a3ae

{% if old_tmp_relation is not none %}
{% do drop_relation(old_tmp_relation) %}
{% endif %}
{%- set empty_sql = 'SELECT * FROM (' ~ compiled_code ~ ') _dbt_sbq WITH NO DATA' -%}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, a newline before the closing parenthesis is needed if there is a possibility that compiled_code could end with a comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed it 👍
f95a3ae

Signed-off-by: Daisuke Taniwaki <daisuketaniwaki@gmail.com>
…style

Signed-off-by: Daisuke Taniwaki <daisuketaniwaki@gmail.com>
@dtaniwaki
dtaniwaki force-pushed the feat/athena-build-with-subquery branch from 0b73fd6 to c2217e9 Compare April 8, 2026 16:28
@dtaniwaki
dtaniwaki requested a review from iconara April 8, 2026 16:29
…ormalization

Signed-off-by: Daisuke Taniwaki <daisuketaniwaki@gmail.com>
dtaniwaki added a commit to dtaniwaki/dbt-adapters that referenced this pull request Apr 9, 2026
PR dbt-labs#1830/dbt-labs#1832 のテストが fork #3 で追加された disable_batch_fallback
引数を MockAdapter.run_query_with_partitions_limit_catching に含んでいなかった。
@colin-k-rogers colin-k-rogers self-assigned this Apr 9, 2026
@dtaniwaki

dtaniwaki commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

@colin-rogers-dbt @iconara Could you review this PR? I think direct subquery insert also solves many situations of HIVE_TOO_MANY_PARTITIONS and ICEBERG_TOO_MANY_OPEN_PARTITIONS.

@iconara iconara left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar enough with the way the jinja templates for CTAS/INSERT/MERGE worked before to say anything about that part, but based on the tests I think this is sound. I left a philosophical comment about naming, and how looking at this from an outside perspective, this new way of handling incremental loads is probably how most would expect it to work by default, and the config naming should reflect that (the current default behavior needs to stay the current default, but naming can be used to signal what the "basic" or simplest behavior is).

build_with_subquery: Use a subquery directly instead of staging data into __dbt_tmp.
Creates an empty tmp table (WITH NO DATA) for schema comparison, then applies via subquery.
Supported for Iceberg merge and append strategies, and Hive append. Incompatible with force_batch.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you aren't familiar with dbt-athena since before this change, you may be confused by the naming of this property. The name is a response to the current default, but the current default is actually not the default behavior that you would expect.

It's natural to add a feature with a config that enables it, but I'm thinking that in this case we are adding something that probably should have been the default mode, and it's only for historical reasons it ended up being added later. At least to me, using a temp table is the alternative way of doing this.

One way to convey this is to use a config that is not true or false, but enables one of two (or more) modes, like incremental_strategy with values "subquery" or "tmp_table" (default).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable. incremental_strategy is the same as dbt-core's config like incremental_strategy: insert_overwrite. How about build_strategy?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about applying this mode to table materialization? This behavior benefits all the materialization types rather than the incremental materialization.

I made another PR over this PR in dtaniwaki#12.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_strategy sounds good to me, that works for table materialization too.

{% do exceptions.raise_compiler_error('build_with_subquery is not supported with Python models.') %}
{% endif %}
{% if force_batch %}
{% do exceptions.raise_compiler_error('build_with_subquery is incompatible with force_batch. Batching requires data in the tmp table.') %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I would say "Batching requires a temp table"

@dtaniwaki dtaniwaki changed the title feat(athena): add build_with_subquery config for incremental merge and append feat(athena): add build_strategy config for incremental and table materializations Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla:yes The PR author has signed the CLA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add option to skip tmp table staging for Athena incremental models

4 participants