Starrocks medallion architecture
Skill ivanshamaev/de-agent-skills/group_skills/starrocks_group_skills/starrocks_medallion_architecture
StarRocks Medallion (Bronze/Silver/Gold) architecture — Bronze raw ingestion (Duplicate Key), Silver cleansed layer (Primary Key upsert/dedup), Gold aggregated layer (Aggregate Key or MV), incremental transform patterns (INSERT OVERWRITE partition, watermark-based), layer DDL patterns, CDC Bronze landing, dbt-compatible layer design, inter-layer INSERT SELECT pipelinesFrom its SKILL.md
npx -y skills add ivanshamaev/de-agent-skills --skill starrocks_medallion_architectureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.2 KB, ~3.0k tokens by cl100k_base, as published. Nobody here has run it
StarRocks Medallion Architecture
When to Use
- Designing a multi-layer data platform on StarRocks
- Structuring Bronze (raw) → Silver (cleansed) → Gold (aggregated) tables
- Building incremental pipelines between layers
- Choosing the right StarRocks table type per layer
- Integrating CDC streams into the medallion architecture
Layer Design Principles
| Layer | Purpose | StarRocks Table Type | Load Pattern |
|---|---|---|---|
| Bronze | Raw, immutable ingestion | Duplicate Key | Broker Load / Stream Load / Routine Load |
| Silver | Cleansed, deduplicated, enriched | Primary Key (upsert) | INSERT OVERWRITE or incremental merge |
| Gold | Business aggregations, metrics | Aggregate Key or MV | INSERT OVERWRITE or Materialized View refresh |
Bronze Layer (Raw Ingestion)
Bronze tables store raw data as-is — no transformation, immutable history.
-- Bronze: raw CDC events, Duplicate Key preserves all versions
CREATE TABLE bronze.orders_raw (
ingestion_id BIGINT NOT NULL, -- auto-increment surrogate
order_id BIGINT NOT NULL,
customer_id BIGINT,
amount DECIMAL(10, 2),
status VARCHAR(32),
op VARCHAR(4), -- CDC op: c/u/d/r
source_ts DATETIME(6), -- source timestamp
kafka_topic VARCHAR(128),
kafka_partition INT,
kafka_offset BIGINT,
ingested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
DUPLICATE KEY(ingestion_id, order_id) -- Duplicate: keep all rows
PARTITION BY RANGE(ingested_at) (
PARTITION p202401 VALUES LESS THAN ("2024-02-01"),
PARTITION p202402 VALUES LESS THAN ("2024-03-01")
)
DISTRIBUTED BY HASH(order_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"storage_medium" = "HDD" -- cold storage for raw
);
-- Bronze ingestion via Routine Load from Kafka
CREATE ROUTINE LOAD bronze_cdc.orders_raw_load ON orders_raw
PROPERTIES (
"desired_concurrent_number" = "4",
"format" = "json",
"jsonpaths" = "[\"$.order_id\",\"$.customer_id\",\"$.amount\",\"$.status\",\"$.op\",\"$.ts_ms\"]",
"columns" = "order_id,customer_id,amount,status,op,source_ts_ms,source_ts=FROM_UNIXTIME(source_ts_ms/1000),ingestion_id=0,ingested_at=NOW()",
"max_error_number" = "1000"
)
FROM KAFKA (
"kafka_broker_list" = "kafka:9092",
"kafka_topic" = "postgres.sales.orders",
"property.group.id" = "sr_bronze_orders"
);
Silver Layer (Cleansed + Deduplicated)
Silver tables hold the current state — deduplicated, enriched, with PK enforcement.
-- Silver: current state, Primary Key for upsert semantics
CREATE TABLE silver.orders (
order_id BIGINT NOT NULL,
customer_id BIGINT NOT NULL,
amount DECIMAL(10, 2),
status VARCHAR(32),
created_at DATETIME,
updated_at DATETIME,
-- Enrichment columns
customer_tier VARCHAR(16),
region VARCHAR(64),
-- Metadata
_bronze_ts DATETIME, -- when this row first appeared in bronze
_silver_ts DATETIME -- when last upserted into silver
)
PRIMARY KEY(order_id)
PARTITION BY RANGE(created_at) (
START ("2024-01-01") END ("2025-01-01") EVERY (INTERVAL 1 MONTH)
)
DISTRIBUTED BY HASH(order_id) BUCKETS 16
PROPERTIES (
"enable_persistent_index" = "true",
"replication_num" = "3"
);
-- Silver: incremental load from Bronze (latest op per order_id)
-- Run every N minutes via Airflow
INSERT INTO silver.orders
SELECT
b.order_id,
b.customer_id,
b.amount,
b.status,
b.source_ts AS created_at,
b.source_ts AS updated_at,
c.tier AS customer_tier,
c.region,
b.ingested_at AS _bronze_ts,
NOW() AS _silver_ts
FROM (
-- Deduplicate: take latest op per order_id from Bronze
SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY source_ts DESC, ingested_at DESC) AS rn
FROM bronze.orders_raw
WHERE op IN ('c', 'u', 'r') -- exclude DELETEs for upsert
AND ingested_at >= DATE_SUB(NOW(), INTERVAL 30 MINUTE)
) t
WHERE rn = 1
) b
LEFT JOIN silver.dim_customers c ON b.customer_id = c.customer_id;
-- Primary Key table: INSERT automatically upserts on order_id
Handling Deletes in Silver
-- Load delete events separately using __op column
INSERT INTO silver.orders
SELECT
order_id,
customer_id,
amount,
status,
source_ts,
source_ts,
NULL, NULL, ingested_at, NOW(),
1 AS __op -- 1 = DELETE for StarRocks PK table
FROM bronze.orders_raw
WHERE op = 'd'
AND ingested_at >= DATE_SUB(NOW(), INTERVAL 30 MINUTE);
Gold Layer (Business Aggregations)
Gold layer provides business-ready metrics, often implemented as Aggregate Key tables or Materialized Views.
-- Gold: daily order metrics (Aggregate Key)
CREATE TABLE gold.orders_daily (
dt DATE NOT NULL,
customer_id BIGINT NOT NULL,
region VARCHAR(64) NOT NULL,
-- Metrics (Aggregate Key functions)
order_count BIGINT SUM DEFAULT "0",
total_revenue DECIMAL(18,2) SUM DEFAULT "0",
avg_amount DECIMAL(10,2) REPLACE DEFAULT "0",
first_order_ts DATETIME MIN DEFAULT "9999-12-31",
last_order_ts DATETIME MAX DEFAULT "1970-01-01"
)
AGGREGATE KEY(dt, customer_id, region)
PARTITION BY RANGE(dt) (
START ("2024-01-01") END ("2025-01-01") EVERY (INTERVAL 1 MONTH)
)
DISTRIBUTED BY HASH(customer_id) BUCKETS 8
PROPERTIES ("replication_num" = "3");
-- Gold: daily refresh via INSERT OVERWRITE
INSERT OVERWRITE gold.orders_daily PARTITION (dt='{{ ds }}')
SELECT
DATE(o.created_at) AS dt,
o.customer_id,
COALESCE(c.region, 'unknown') AS region,
COUNT(*) AS order_count,
SUM(o.amount) AS total_revenue,
AVG(o.amount) AS avg_amount,
MIN(o.created_at) AS first_order_ts,
MAX(o.updated_at) AS last_order_ts
FROM silver.orders o
LEFT JOIN silver.dim_customers c ON o.customer_id = c.customer_id
WHERE DATE(o.created_at) = '{{ ds }}'
GROUP BY DATE(o.created_at), o.customer_id, COALESCE(c.region, 'unknown');
Materialized View as Gold Layer
For frequently queried metrics, use async MV for automatic refresh:
-- Async MV: Gold-level metric, refreshes on schedule
CREATE MATERIALIZED VIEW gold_mv.revenue_by_region
DISTRIBUTED BY HASH(region) BUCKETS 8
REFRESH ASYNC EVERY (INTERVAL 1 HOUR)
AS
SELECT
DATE_TRUNC('day', o.created_at) AS day,
c.region,
COUNT(*) AS order_count,
SUM(o.amount) AS revenue
FROM silver.orders o
JOIN silver.dim_customers c ON o.customer_id = c.customer_id
WHERE o.status != 'cancelled'
GROUP BY DATE_TRUNC('day', o.created_at), c.region;
Inter-Layer Pipeline Orchestration
Full pipeline: Bronze → Silver → Gold, orchestrated by Airflow:
from airflow.decorators import dag, task
from airflow.providers.mysql.hooks.mysql import MySqlHook
from datetime import datetime, timedelta
@dag(
dag_id="medallion_pipeline",
schedule="0 6 * * *",
start_date=datetime(2024, 1, 1),
catchup=True,
max_active_runs=1,
tags=["starrocks", "medallion"],
)
def medallion_pipeline():
@task
def bronze_to_silver(ds: str = None):
hook = MySqlHook(mysql_conn_id="starrocks_prod")
# Watermark: process bronze rows ingested in this interval
# Silver is PK: INSERT upserts, covering all ops
hook.run(f"""
INSERT INTO silver.orders
SELECT
order_id, customer_id, amount, status,
source_ts AS created_at, source_ts AS updated_at,
NULL AS customer_tier, NULL AS region,
ingested_at AS _bronze_ts, NOW() AS _silver_ts
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY source_ts DESC) AS rn
FROM bronze.orders_raw
WHERE DATE(ingested_at) = '{ds}'
AND op != 'd'
) t
WHERE rn = 1
""")
@task
def silver_to_gold(ds: str = None):
hook = MySqlHook(mysql_conn_id="starrocks_prod")
hook.run(f"""
INSERT OVERWRITE gold.orders_daily PARTITION (dt='{ds}')
SELECT
DATE(created_at) AS dt,
customer_id,
COALESCE(region, 'unknown') AS region,
COUNT(*) AS order_count,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_amount,
MIN(created_at) AS first_order_ts,
MAX(updated_at) AS last_order_ts
FROM silver.orders
WHERE DATE(created_at) = '{ds}'
GROUP BY DATE(created_at), customer_id, COALESCE(region, 'unknown')
""")
@task
def validate_layer(layer: str, ds: str = None, min_rows: int = 100):
hook = MySqlHook(mysql_conn_id="starrocks_prod")
table = {"bronze": "bronze.orders_raw", "silver": "silver.orders", "gold": "gold.orders_daily"}[layer]
date_col = "ingested_at" if layer == "bronze" else ("created_at" if layer == "silver" else "dt")
count = hook.get_records(
f"SELECT COUNT(*) FROM {table} WHERE DATE({date_col}) = '{ds}'"
)[0][0]
if count < min_rows:
raise ValueError(f"{layer} layer: only {count} rows for {ds}, expected >= {min_rows}")
print(f"{layer} layer validation passed: {count} rows")
b2s = bronze_to_silver()
s2g = silver_to_gold()
val_s = validate_layer.override(task_id="validate_silver")(layer="silver")
val_g = validate_layer.override(task_id="validate_gold")(layer="gold")
b2s >> val_s >> s2g >> val_g
dag = medallion_pipeline()
Schema Conventions
| Convention | Bronze | Silver | Gold |
|---|---|---|---|
| Partition column | ingested_at (load time) | created_at (business time) | dt (day) |
| Primary key | ingestion_id (surrogate) | Business key (order_id) | Dimension keys |
| Null handling | Allow nulls | Coalesce nulls | COALESCE in aggregation |
| CDC op column | Keep op column | Handle in load logic | Excluded |
| Retention | 90 days → cold storage | 2 years | 5 years |
| Storage medium | HDD (cheaper) | SSD for hot, HDD for cold | SSD |
Anti-Patterns
- Applying transforms in Bronze — Bronze is a raw landing zone; any bug in a transform requires reprocessing from source. Keep Bronze immutable.
- Using Aggregate Key in Silver — Silver needs upsert semantics for CDC; Aggregate Key cannot express record replacement. Use Primary Key.
- INSERT INTO (append) for Gold — daily partition re-runs append rather than replace; always use
INSERT OVERWRITEfor Gold partitions. - No deduplication in Bronze → Silver — CDC streams often produce multiple ops for the same key; always deduplicate with ROW_NUMBER before merging into Silver.
- Refreshing Gold MVs too frequently — MV refresh consumes BE resources; tune to match business SLA, not maximum frequency.
- Missing
__opfor DELETE propagation — If source deletes should flow to Silver, explicitly loadop=drows with__op=1header; omitting this causes deleted records to persist.
References
- StarRocks table types:
docs.starrocks.io/docs/table_design/table_types/ - INSERT OVERWRITE:
docs.starrocks.io/docs/loading/InsertInto/ - Materialized Views:
docs.starrocks.io/docs/using_starrocks/Materialized_view/ - Related skills:
[[starrocks-ddl-table-types]],[[starrocks-partitioning]],[[starrocks-materialized-views]],[[starrocks-cdc-pipeline]],[[medallion-architecture]]
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most architecture codebase skills give in ~3.0k tokens
Counted across 811 of the 1,134 authors here whose files we hold, read 2026-08-07
- Ask the user which candidate to explorein 45 of 811, across 15 files
- Apply the deletion test to suspected shallow modulesin 43 of 811, across 15 files
- Read any relevant architecture decision records firstin 31 of 811, across 8 files
- Use exact glossary terms in every suggestionin 30 of 811, across 10 files
- Accept dependencies instead of creating themin 24 of 811, across 5 files
- Include before and after visualisations for each candidatein 24 of 811, across 5 files
- Read the domain glossary before exploringin 24 of 811, across 6 files
- Return results instead of producing side effectsin 23 of 811, across 4 files
- Explore the codebase for shallow modules and frictionin 23 of 811, across 3 files
- Introduce seams only where things varyin 22 of 811, across 3 files
- Reduce the number of methodsin 21 of 811, across 2 files
- Design deep modules with small interfacesin 21 of 811, across 3 files
Said here and by no other author read
- Use Duplicate Key for Bronze tables
- Keep Bronze tables immutable
- Use Primary Key for Silver tables
- Use Aggregate Key or Materialized Views for Gold tables
- Use INSERT OVERWRITE for Gold partitions
- Deduplicate Bronze data before loading Silver
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.