Skip to main content
Last updated on

Apache Doris dbt Adapter

dbt manages the transformation step in an ELT (Extract, Load, Transform) workflow. The dbt-for-apache-doris adapter compiles dbt models into Doris SQL and executes table creation, transformation, testing, and documentation operations through the Doris Frontend (FE) MySQL query port.

Source data must already be loaded into Doris. The adapter performs transformations; it does not ingest or synchronize source data.

caution

dbt-for-apache-doris is provided and maintained by VeloDB. It is not part of the Apache Doris project and is not released or endorsed by the Apache Doris community. Evaluate the adapter before using it in production, verify the integrity of the package and its release information, and follow the license of the third-party project. Report adapter issues to the dbt-for-apache-doris project.

Environment requirements

ComponentRequirement
Python3.10 or later

Released versions

Version
1.1.0

Installation

The following commands use the published v1.1.0 release as an installation example. To install another release, replace the version in the pip requirement.

Install the adapter from PyPI in an isolated Python environment:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "dbt-for-apache-doris==1.1.0"
dbt --version

On Windows PowerShell:

py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "dbt-for-apache-doris==1.1.0"
dbt --version

For this example, the Plugins section of dbt --version should contain doris: 1.1.0. dbt Core and MySQL Connector/Python are installed as dependencies.

Configure the connection

Create a dbt project:

dbt init doris_demo
cd doris_demo

Add a Doris output to ~/.dbt/profiles.yml. Keep credentials outside version control; this example reads the password from an environment variable:

doris_demo:
target: dev
outputs:
dev:
type: doris
host: 127.0.0.1
port: 9030
username: dbt_user
password: "{{ env_var('DORIS_PASSWORD') }}"
schema: analytics
threads: 4
SettingRecommended valueBehavior
typeMust be dorisSelects the Doris adapter
hostSet the Doris FE address explicitlyThe underlying credentials fall back to 127.0.0.1 when omitted; dbt init has no default input
portUsually 9030Defaults to 9030 when omitted; this is the FE query port, not the HTTP port
usernameSet a dedicated Doris user explicitlyThe underlying credentials fall back to root when omitted; dbt init has no default input
passwordRead from an environment variableDefaults to an empty string
schemaSet the target Doris databaseThe dbt init prompt defaults to dbt; the underlying credentials default to None
threadsSet according to FE and workload capacityThe dbt init prompt defaults to 1
databaseOmit itIf set, it must exactly equal schema

The adapter maps a dbt schema to a Doris database. If the target database does not exist, the adapter attempts to create it. The Doris account used by dbt therefore needs permission to read source tables and to create, alter, drop, and write objects in the target database.

Here, database is not a Doris catalog. It is a field inherited from dbt's generic database.schema.identifier naming model. Doris Internal Catalog uses only database.table, so set the Doris database in profile schema and omit profile database. Source database has a different compatibility behavior: it can override schema as the Doris database for cross-database reads. Profiles and standard source() cannot represent both a Doris catalog and database level.

caution

The adapter currently does not support External Catalog three-part namespaces using Catalog, Database, and Table.

Ensure that dbt_project.yml references the same profile:

name: doris_demo
version: "1.0.0"
config-version: 2
profile: doris_demo

model-paths: ["models"]

models:
doris_demo:
+materialized: view

On macOS or Linux, validate the connection with:

export DORIS_PASSWORD='your_password'
dbt debug

On Windows PowerShell:

$env:DORIS_PASSWORD = 'your_password'
dbt debug

Build the first model

The following SQL creates a source table for this example. A replication number of 1 is appropriate only for a single-BE development cluster:

CREATE DATABASE IF NOT EXISTS raw;

CREATE TABLE IF NOT EXISTS raw.orders (
order_id BIGINT,
customer_id BIGINT,
order_time DATETIME,
amount DECIMAL(18, 2),
status VARCHAR(20),
updated_at DATETIME,
is_valid BOOLEAN
)
DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 1
PROPERTIES ("replication_num" = "1");

INSERT INTO raw.orders VALUES
(1, 101, '2026-08-01 10:00:00', 20.00, 'PAID', '2026-08-01 10:05:00', true),
(2, 102, '2026-08-01 11:00:00', 35.00, 'PAID', '2026-08-01 11:05:00', true);

Declare the source in models/sources.yml:

version: 2

sources:
- name: raw
schema: raw
tables:
- name: orders

Create models/fct_daily_sales.sql:

{{
config(
materialized='table',
duplicate_key=['order_date'],
distributed_by=['order_date'],
buckets=1,
properties={'replication_num': '1'}
)
}}

select
cast(order_time as date) as order_date,
sum(amount) as sales_amount
from {{ source('raw', 'orders') }}
group by cast(order_time as date)

Add data tests in models/schema.yml:

version: 2

models:
- name: fct_daily_sales
description: Daily sales summary
columns:
- name: order_date
description: Order date
data_tests:
- not_null
- unique
- name: sales_amount
description: Daily sales amount
data_tests:
- not_null

Run the model and tests:

dbt build --select fct_daily_sales

A successful build creates analytics.fct_daily_sales. Set the production replication and bucket counts according to your cluster deployment.

When a source is in another Doris database, prefer setting schema directly:

sources:
- name: finance
schema: finance_raw
tables:
- name: payments

For compatibility with existing dbt projects, a source may instead set only database: finance_raw, or set database and schema to the same value. If source database and schema differ, the adapter uses database as the Doris database. This differs from profile validation, which requires both values to match. New projects should consistently use source schema to avoid ambiguity.

Materializations

dbt-for-apache-doris supports these main materializations:

MaterializationDoris object or behaviorUse case
viewDoris viewLightweight transformations that should always read current source data
tableA fully rebuilt Doris Duplicate Key tableBounded results that need stable query performance
incrementalAppends, upserts, or overwrites data according to a strategyIncremental processing for large tables
materialized_viewDoris asynchronous materialized viewDoris-managed refresh and transparent query rewrite
ephemeralCompiles into a CTE in downstream modelsReuse SQL without creating a Doris object

Seeds and snapshots can also create Doris tables. The adapter currently does not include a custom partition materialization; use incremental insert_overwrite for partition replacement. The examples below keep replication_num=1 so they also run on a single-BE development cluster; use a production-appropriate replica count in real deployments.

View

{{ config(materialized='view') }}

select order_id, customer_id, amount
from {{ source('raw', 'orders') }}
where status = 'PAID'

Each run updates the view definition. A view stores no data; its query cost depends on the view SQL and downstream query.

Table

{{
config(
materialized='table',
duplicate_key=['event_date', 'event_id'],
partition_by=['event_date'],
partition_type='RANGE',
partition_by_init=[
"PARTITION p_before_202608 VALUES LESS THAN ('2026-08-01')",
"PARTITION p202608 VALUES LESS THAN ('2026-09-01')",
"PARTITION pmax VALUES LESS THAN (MAXVALUE)"
],
distributed_by=['event_id'],
buckets=16,
properties={'replication_num': '1'}
)
}}

select
cast(order_time as date) as event_date,
order_id as event_id,
status as event_type
from {{ source('raw', 'orders') }}

The table materialization creates a Duplicate Key table and fully replaces the target on subsequent runs. Table models accept the following table-creation settings:

SettingType and defaultDescription
duplicate_keyString or list; optionalDuplicate Key columns
partition_byString or list; optionalPartition columns
partition_typeRANGE or LIST; default RANGEPartition type used with partition_by
partition_by_initList of strings; optionalDoris partition definitions used with partition_by at table creation
distributed_byString or list; optionalHash distribution columns
bucketsPositive integer; defaults to 10 when Hash Distribution is emittedHash bucket count; used only with distributed_by
replication_numPositive integer or numeric string; optionalReplica count; the top-level setting overrides the same key in properties
propertiesDictionary; optionalKey-value pairs rendered into Doris PROPERTIES

Duplicate Key, partition, and distribution columns must be present in the model output and comply with Doris table-creation rules. The adapter renders partition_by_init and properties into the CREATE TABLE statement; Doris validates the partition definitions, property names, and values. An ordinary table model currently does not expose Aggregate Key or standalone Unique Key configuration. Use incremental merge for Unique Key upserts.

Incremental

For append, merge, and insert_overwrite, an incremental model has two parts:

  • Model SQL, usually with is_incremental(), determines which source rows the current run returns.
  • incremental_strategy determines how the adapter writes that batch to the target.

Microbatch does not use this filtering pattern. dbt Core injects time filters from the batch context and the event_time configured on upstream resources.

StrategyDoris target and behaviorMain requirements
appendExecutes INSERT INTO against a Duplicate Key tableDo not set unique_key
mergePerforms full-row INSERT INTO upserts against a MOW or MOR Unique Key tableunique_key is required
insert_overwriteOverwrites the whole table, named partitions, or partitions touched by the batchunique_key is rejected
microbatchOverwrites one exact RANGE partition for each dbt Core UTC windowConfigure event_time, batch_size, and begin

When no strategy is set, unique_key selects merge; otherwise the adapter uses append. The adapter supports all four on_schema_change modes: ignore, fail, append_new_columns, and sync_all_columns.

delete+insert and delete_insert are rejected. Use merge for Doris Unique Key upserts. merge does not generate SQL MERGE INTO; it uses full-row INSERT INTO with Doris Unique Key semantics. Each batch must contain at most one row for each unique_key. To use a Sequence Column, set the visible column through function_column.sequence_col in properties and continue using merge. The bare sequence_col setting and function_column.sequence_type with the hidden __DORIS_SEQUENCE_COL__ are currently not supported.

The following example performs a full-row upsert by order_id:

{{
config(
materialized='incremental',
incremental_strategy='merge',
unique_key=['order_id'],
distributed_by=['order_id'],
buckets=16,
properties={'replication_num': '1'},
on_schema_change='append_new_columns'
)
}}

select
order_id,
customer_id,
amount,
updated_at
from {{ source('raw', 'orders') }}

{% if is_incremental() %}
where updated_at >= (
select coalesce(max(updated_at), '1970-01-01 00:00:00')
from {{ this }}
)
{% endif %}

To overwrite named Doris partitions:

{{
config(
materialized='incremental',
incremental_strategy='insert_overwrite',
duplicate_key=['event_date', 'event_id'],
partition_by=['event_date'],
partition_type='RANGE',
partition_by_init=[
"PARTITION p_before_202607 VALUES LESS THAN ('2026-07-01')",
"PARTITION p202607 VALUES LESS THAN ('2026-08-01')",
"PARTITION p202608 VALUES LESS THAN ('2026-09-01')",
"PARTITION pmax VALUES LESS THAN (MAXVALUE)"
],
overwrite_partitions=['p202607', 'p202608'],
distributed_by=['event_id'],
buckets=16,
properties={'replication_num': '1'}
)
}}

select event_date, event_id, event_type
from (
select
cast(order_time as date) as event_date,
order_id as event_id,
status as event_type,
order_time
from {{ source('raw', 'orders') }}
) events

{% if is_incremental() %}
where order_time >= '2026-07-01'
and order_time < '2026-09-01'
{% endif %}

The first run creates the target through CTAS and does not execute INSERT OVERWRITE. The example therefore uses partition_by_init to create every required target partition. Once the target exists, overwrite_partitions controls the overwrite scope:

  • Omitted: overwrite the whole table.
  • List of partition names: overwrite only those partitions.
  • '*': let Doris dynamically overwrite partitions touched by the batch.

A dynamic overwrite cannot infer which partition to clear from an empty batch. Specify partition names when an empty source batch must clear target data.

Microbatch

microbatch uses the dbt Core batch context. Configure an event_time field on each direct upstream resource and on the target model. For the raw.orders source created in the first-model example above, add this configuration to its existing table declaration:

sources:
- name: raw
schema: raw
tables:
- name: orders
config:
event_time: order_time
{{
config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='event_time',
batch_size='day',
begin=modules.datetime.datetime(2026, 8, 1, 0, 0, 0),
duplicate_key=['event_id', 'event_time'],
partition_by=['event_time'],
partition_type='RANGE',
distributed_by=['event_id'],
buckets=16,
properties={'replication_num': '1'}
)
}}

select
order_id as event_id,
order_time as event_time,
status as event_type
from {{ source('raw', 'orders') }}

event_time must be an unquoted column name and must refer to the same single column as partition_by. batch_size supports hour, day, month, and year. The target is a Duplicate Key table: duplicate_key configures the Doris table key and is not dbt unique_key. Do not set unique_key, overwrite_partitions, or partition_by_init; the adapter derives the partition boundary and overwrite target from the current batch. If an upstream ref() or source() also configures event_time, dbt Core filters that input to the current window. Microbatch batches execute serially and overwrite the exact partition even when a batch is empty, so rows removed from that window are cleared.

Microbatch can also use Doris Dynamic Partition when the following properties are configured and pass adapter validation:

  • dynamic_partition.enable='true'.
  • dynamic_partition.time_unit matches batch_size, and dynamic_partition.time_zone is UTC, Etc/UTC, or +00:00.
  • dynamic_partition.create_history_partition='true', with either dynamic_partition.start or dynamic_partition.history_partition_num.
  • dynamic_partition.prefix is a valid identifier, and dynamic_partition.end is a positive integer.
  • For monthly batches, dynamic_partition.start_day_of_month, when set, is 1.

Doris also requires a positive dynamic_partition.buckets value when creating a Dynamic Partition table; Doris validates this property rather than the adapter. The retention window must cover begin, lookback, and any manual backfill range. For an existing target, the physical properties validated by the adapter must match the model configuration; otherwise align the configuration or run --full-refresh.

caution

For append, merge, and partition-scoped insert_overwrite, the adapter does not discover new or changed source rows automatically. The model SQL must return the intended batch. Whole-table insert_overwrite must return the complete target data. Each merge batch must contain unique unique_key values. dbt Core supplies Microbatch filtering from upstream resources configured with event_time, so Microbatch models do not need is_incremental(). Run dbt run --full-refresh after incompatible key, partition, or physical-layout changes.

Asynchronous materialized views

dbt users configure the standard materialized='materialized_view'. The adapter implements it with a Doris asynchronous materialized view.

note

The FE version gate for this materialization accepts Doris 2.x at 2.1.5 or later, Doris 3.x except 3.0.0, Doris 4.x and later, and identifiable source builds reported as doris-0.0.0-<git-sha>. Source builds are for development testing only. These gates are code admission conditions, not evidence that every accepted version has passed compatibility testing.

{{
config(
materialized='materialized_view',
build_mode='immediate',
refresh_method='auto',
refresh_trigger='manual',
duplicate_key=['order_date'],
distribution_type='hash',
distributed_by=['order_date'],
buckets=8,
wait_for_refresh=true,
properties={'replication_num': '1'}
)
}}

select order_date, sales_amount
from {{ ref('fct_daily_sales') }}
SettingDefaultSupported values or format
build_modeimmediateimmediate, deferred
refresh_methodautoauto lets Doris determine the refresh scope; complete refreshes all partitions
refresh_triggermanualmanual, schedule, commit
refresh_schedule-Required for schedule; contains a positive interval, a minute/hour/day/week unit, and optional start_time
wait_for_refreshtrueWhether to wait for the BUILD IMMEDIATE initial build or an adapter-submitted manual refresh
refresh_wait_timeout300Refresh task timeout in seconds; positive integer
refresh_poll_interval1Refresh task polling interval in seconds; positive integer no greater than the timeout
duplicate_key-Duplicate Key columns for the asynchronous materialized view; string or list
partition_by-One Doris partition column or partition-function expression; string or one-element list
distribution_typeSelected automaticallyhash when distributed_by is set; otherwise random
distributed_by-Hash Distribution columns
bucketsautoPositive integer or auto
replication_num-Replica-count convenience setting; overrides the same key in properties
properties{}Doris asynchronous materialized-view properties
on_configuration_changeapplyapply, continue, fail

BUILD IMMEDIATE starts the initial build when a definition is created or replaced. The adapter creates the new definition under a temporary name, waits for the initial build by default, and then publishes it at the target name; it does not submit an extra REFRESH MATERIALIZED VIEW. BUILD DEFERRED creates the definition without an initial build.

When the definition has not changed, refresh behavior is controlled entirely by refresh_trigger; there is currently no refresh_on_run configuration:

  • manual submits REFRESH MATERIALIZED VIEW ... AUTO|COMPLETE when a later dbt run selects the model and waits for success by default.
  • schedule and commit do not submit a refresh from a later dbt run; Doris triggers it from the schedule or base-table commit.

For example, configure a daily schedule with:

{{
config(
materialized='materialized_view',
build_mode='deferred',
refresh_method='auto',
refresh_trigger='schedule',
refresh_schedule={
'interval': 1,
'unit': 'day'
},
properties={'replication_num': '1'}
)
}}

select order_date, sales_amount
from {{ ref('fct_daily_sales') }}

wait_for_refresh=false disables task polling; it does not suppress the refresh request. A dbt wait timeout does not cancel an already submitted Doris task. The adapter identifies a newly submitted task by comparing Doris MV task IDs before and after submission, so avoid concurrent manual refreshes of the same target.

When model SQL or DDL configuration changes, the default on_configuration_change=apply builds a temporary asynchronous materialized view and atomically replaces the existing definition. continue retains the existing definition without submitting a manual refresh, while fail stops the run. --full-refresh forces the definition to be redeployed.

Other dbt capabilities

CapabilitySupport
SeedPut small, version-controlled CSV files in seeds/, then run dbt seed; supports type inference, column_types, and ref
SnapshotDefine check or timestamp strategies in snapshots/, then run dbt snapshot; supports hard deletes, schema evolution, and atomic replacement
Data testsConfigure Generic Tests in YAML or write Singular Tests in tests/; supports ephemeral and store_failures paths
Unit testsProvide inline-row or CSV fixtures in model YAML, then run dbt test --select test_type:unit
Model contractsSet contract.enforced: true and declare column names and types for Table, View, or Incremental models; does not create database PK or NOT NULL constraints
Persisted docsEnable persist_docs for relation and column comments on primary relation types
Source freshnessConfigure loaded_at_field or loaded_at_query with freshness, then run dbt source freshness
GrantsUse standard grants configuration for Doris user and user@host table privileges; role principals are currently not supported
HooksUse pre_hook and post_hook to execute Doris SQL around materializations; hook side effects have no transactional rollback
Documentation and lineageWrite descriptions and run dbt docs generate for metadata about Doris databases, tables, views, columns, comments, and asynchronous materialized views

Seeds are suitable for small, version-controlled data such as country codes or status mappings, not bulk business-data ingestion. Use Doris Stream Load, Broker Load, or a Doris Connector for large files.

Snapshot source unique_key values must be non-NULL and unique within each batch. With the timestamp strategy, updated_at must be non-NULL and cannot precede the current historical version for that key.

To persist table and column descriptions:

models:
- name: fct_daily_sales
description: Daily sales summary
config:
persist_docs:
relation: true
columns: true
columns:
- name: order_date
description: Order date
- name: sales_amount
description: Daily sales amount

Source freshness

Source Freshness supports two explicit timestamp paths. Use loaded_at_field for the common timestamp-column case:

sources:
- name: raw
schema: raw
tables:
- name: orders
config:
loaded_at_field: updated_at
freshness:
warn_after: {count: 1, period: hour}
error_after: {count: 2, period: hour}

Use loaded_at_query when the freshness timestamp requires custom SQL or aggregation:

sources:
- name: raw
schema: raw
tables:
- name: orders
config:
loaded_at_query: |
select max(updated_at)
from {{ this }}
where is_valid = 1
freshness:
warn_after: {count: 1, period: hour}
error_after: {count: 2, period: hour}

When using loaded_at_field, set filter under freshness, such as filter: is_valid = 1, to limit which rows participate in freshness evaluation. Do not configure loaded_at_field and loaded_at_query together. The adapter currently does not infer load time from Doris table metadata; provide a field or query explicitly.

The adapter uses utc_timestamp() as the current time, matching dbt Core's convention of evaluating timezone-naive freshness timestamps as UTC.

Grants

Declarative grants use Doris users as principals:

models:
doris_demo:
+grants:
select:
- analyst
- reporter@10.0.0.%

A name without @host is treated as username@%. Privileges map as follows: select to SELECT_PRIV, insert to LOAD_PRIV, alter to ALTER_PRIV, create to CREATE_PRIV, drop to DROP_PRIV, and show_view to SHOW_VIEW_PRIV. Doris roles are currently not supported in grants; configure concrete users. The account running dbt must be able to inspect principals and adjust privileges on target objects.

Common commands

CommandPurpose
dbt debugValidate the project configuration and Doris connection
dbt parseParse the project without executing SQL
dbt compileCompile models and inspect generated Doris SQL
dbt runBuild selected models
dbt testRun data tests and unit tests
dbt buildRun seeds, models, snapshots, and tests in dependency order
dbt seedLoad seed CSV files
dbt snapshotUpdate snapshots
dbt source freshnessEvaluate source freshness
dbt docs generateGenerate catalog, documentation, and lineage metadata

Common selectors:

# Run one model
dbt run --select fct_daily_sales

# Run the model and all upstream dependencies
dbt build --select +fct_daily_sales

# Run the model and all downstream dependencies
dbt build --select fct_daily_sales+

# Fully rebuild an incremental model
dbt run --select fct_orders --full-refresh

Current limitations

  • The adapter currently does not support Aggregate Key table modeling or secondary-index configuration.
  • The adapter currently does not support a complete External Catalog namespace.
  • The adapter currently does not implement SSL configuration, timeout/retry, multi-FE failover, server-side cancellation, or complete query telemetry.
  • Some Table/View/MV type changes have a short canonical-name availability window rather than a zero-downtime switch.

Troubleshooting

dbt debug cannot connect

Verify that host and port point to a reachable FE MySQL query port, and check the username, password, and network policy. The default query port is 9030; 8030 is normally the FE HTTP port.

database and schema differ

Profile schema is the target Doris database. Remove database; it is not a Doris catalog and does not add another namespace level. If you retain this compatibility field, set it to exactly the same value as schema.