Physical design¶
Physical design controls target-specific DDL: concrete column types, partitioning, indexes, clustering, table engines, compression, and storage settings. It complements schema contracts: contracts are portable, physical design is sink-specific.
Configuration¶
sink:
options:
physical_design:
enabled: true
mode: auto
apply: online
apply_runtime: true
reconciliation:
mode: block
columns:
status:
target_type:
clickhouse: LowCardinality(String)
partitioning:
strategy: auto
column: business_date
granularity: month
indexes:
strategy: auto
primary_key: [order_id]
storage:
mssql:
compression: page
clustered_columnstore: false
filegroup: DATA
textimage_filegroup: LOB
index_fillfactor: 90
postgres:
fillfactor: 90
clickhouse:
engine: MergeTree
partition_by: toYYYYMM(business_date)
order_by: [business_date, order_id]
low_cardinality:
mode: auto
bigquery:
partition_by: business_date
clustering: [customer_id, status]
Apply modes¶
| Mode | Behavior |
|---|---|
online |
Apply only low-risk online-safe changes automatically. |
safe_window |
Allow planned blocking DDL inside a maintenance window. |
plan_only |
Render DDL and risks, do not apply. |
manual_approval |
Require approval artifact before execution. |
New target tables can receive the full physical design. Existing targets only receive online-safe changes automatically. The execution decision is handled by Physical DDL apply runtime, keeping planning, approval, and connector-specific execution testable.
Existing-table drift reconciliation¶
physical_design.reconciliation controls what happens when an existing target
table differs from the desired physical_design contract.
| Mode | Existing-table behavior |
|---|---|
block |
Default. Detect drift, emit blockers/evidence, and stop before load. |
auto_safe |
Apply only target-certified online-safe physical changes, then continue if no blockers remain. |
safe_window |
Apply target-certified blocking changes (for example MSSQL compression REBUILD) when approval evidence is attached. |
plan_only |
Emit the reconciliation plan and block before load; never execute DDL. |
ClickHouse v1 supports auto_safe only for table settings that can be changed
with ALTER TABLE ... MODIFY SETTING:
Table settings manipulations.
Some valid CREATE TABLE ... SETTINGS values, including index_granularity,
are create-time physical design choices and are not treated as existing-table
auto-safe changes.
Engine, partition, sorting key, primary key, and physical column-type drift are
blockers because they require a shadow table migration or schema-evolution
workflow. This keeps runtime loads from silently changing storage layout.
Use the read-only CLI before changing production routes:
dpone schema physical-diff \
--manifest manifests/orders.yaml \
--actual actual-clickhouse-physical.json \
--format json
The actual JSON uses the same portable shape emitted by target introspection:
{
"sink_type": "clickhouse",
"table": "landing.orders",
"engine": "MergeTree",
"partition_by": "toYYYYMM(created_at)",
"order_by": ["created_at", "order_id"],
"columns": {
"order_id": {"type": "Nullable(Int64)"}
},
"table_settings": {
"min_rows_for_wide_part": 0
}
}
Target examples¶
MSSQL¶
sink:
options:
physical_design:
indexes:
primary_key: [order_id]
storage:
mssql:
compression: page
clustered_columnstore: false
Planned SQL shape:
CREATE TABLE [landing].[orders] (...) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX [ix_landing_orders_order_id] ON [landing].[orders] ([order_id]);
For existing tables, compression changes still require a blocking
ALTER TABLE ... REBUILD WITH (DATA_COMPRESSION = PAGE) and therefore
reconciliation.mode: safe_window plus explicit approval evidence:
physical_design:
apply_runtime: true
apply: safe_window
reconciliation:
mode: safe_window
approval:
approved_by: dba-team
approved_risks:
- table_settings.compression
table: DWH_VI.ch.marketing__wa_reg_auth_app
expires_at: "2026-07-11T21:00:00Z"
storage:
mssql:
compression: page
Postgres¶
Planned SQL uses CREATE INDEX CONCURRENTLY for existing-table index paths
where possible.
ClickHouse¶
sink:
options:
physical_design:
storage:
clickhouse:
engine: MergeTree
partition_by: toYYYYMM(business_date)
order_by: [business_date, order_id]
low_cardinality:
mode: auto
max_distinct_values: 10000
max_distinct_ratio: 0.05
ClickHouse ORDER BY and PRIMARY KEY columns should normally be not nullable.
ClickHouse can allow Nullable(...) key expressions with the MergeTree
allow_nullable_key table setting, but its own MergeTree documentation
describes Nullable key expressions as possible but strongly discouraged and
states that NULL values sort with NULLS_LAST semantics:
MergeTree primary keys and indexes.
ClickHouse documents allow_nullable_key as a MergeTree table setting that can
be supplied in the storage-specific SETTINGS clause of CREATE TABLE:
allow_nullable_key,
CREATE TABLE clause order.
dpone best practice:
- Prefer
physical_design.storage.clickhouse.nullability.mode: non_nullable_by_defaultfor inferred ClickHouse key columns. - Use
allow_nullable_keyonly as an explicit compatibility escape hatch when preserving nullable key semantics is more important than ClickHouse's recommended physical design. - Keep table settings separate from insert settings:
allow_nullable_keybelongs to target table creation, notclickhouse_bulk.insert_settings.
Target table-settings syntax:
sink:
options:
physical_design:
storage:
clickhouse:
order_by: [optional_code]
table_settings:
allow_nullable_key: 1
table_settings is the storage-specific target table contract. It affects
CREATE TABLE ... SETTINGS ...; it does not affect data loading. Insert-time
settings still belong to sink.options.clickhouse_bulk.insert_settings, and
inferred type nullability still belongs to
physical_design.storage.clickhouse.nullability.
dpone validates table settings before DDL execution:
- setting names must be safe SQL identifiers;
- values must be scalar strings, numbers, integers, or booleans;
- known wrong-context settings fail with an actionable alternative;
- unknown safe ClickHouse settings are allowed with a warning because ClickHouse table settings evolve between dpone releases.
Wrong-context example:
This fails because async_insert is an insert setting. Use:
BigQuery¶
sink:
options:
physical_design:
storage:
bigquery:
partition_by: business_date
clustering: [customer_id, status]
BigQuery physical design is planned as table creation or controlled recreation. Unsafe changes to existing partitioning/clustering are not applied silently.
ClickHouse LowCardinality modes¶
| Mode | Behavior |
|---|---|
off |
Never generate LowCardinality. |
auto |
Use profiler cardinality thresholds for string-like columns. |
explicit |
Apply only to configured columns. |
force |
Apply to configured columns even when profiler disagrees, with warning. |
preserve |
Keep existing/source LowCardinality, but do not generate new ones. |
Auto mode requires a string-like column and a stable low-cardinality profile:
distinct_count <= 10000distinct_ratio <= 0.05- column name does not look like an identifier, UUID, email, URL, hash, or token
LowCardinality is certified only for string-like columns by default. Numeric,
date, timestamp, binary and JSON columns are not automatically wrapped. If a
future ClickHouse-specific contract needs non-string LowCardinality, add it
as an explicit documented physical override and extend the certification suite
first.
Decision categories in physical plans:
| Example | Category |
|---|---|
physical_design.columns.amount.target_type.clickhouse: String |
explicit_physical_override |
schema_contract.columns.amount.type: decimal |
explicit_logical_contract |
Source metadata numeric(18,4) -> Decimal(18,4) |
auto_inferred |
| No metadata/sample confidence | quarantine_required / safe fallback |
CLI¶
dpone schema physical-plan --manifest manifests/orders.batch.yaml --format md
dpone plan manifests/orders.batch.yaml --selector public.orders --format json
Runbook¶
| Symptom | Action |
|---|---|
| Index/compression DDL is blocked | Switch to safe_window or create an approval artifact. |
| ClickHouse query is slow after load | Review ORDER BY and partition cardinality. |
| LowCardinality hurts performance | Set low_cardinality.mode: off or move the column out of columns. |
| BigQuery partition change is required | Use a shadow/recreate plan, then validate downstream permissions. |
| MSSQL table is write-heavy | Prefer row compression or no compression over page compression. |