본문으로 건너뛰기
cccskills
무료GitHub에 공개

sql-analytics-and-dbt

Use when writing or changing analytical SQL or a dbt model — CTE structure, window functions, grain and join correctness, dbt data tests, unit tests, contracts and incremental models, and how to run them

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md8.0 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

SQL Analytics and dbt

Overview

Analytical SQL fails quietly: a join that fans out, a NULL that drops rows from a NOT IN, a running total over the wrong frame. The query runs, the dashboard shows a number, the number is wrong. dbt gives you the tools to make SQL testable — use them the way you use pytest.

Core principle: every model declares its grain and tests it; logic is proven with a dbt unit test before it is trusted with a real table.

1. Detect the project's conventions

Read dbt_project.yml, packages.yml (is dbt_utils installed?), the adapter in profiles.yml or CI (Postgres, Snowflake, BigQuery, Databricks, DuckDB), any .sqlfluff config, and the existing folder and naming scheme (staging/stg_<source>__<entity>, intermediate/, marts/fct_*, dim_*). Follow them. SQLMesh projects follow SQLMesh's own model and audit conventions — never mix the two.

2. Model structure

with

orders as (
    select * from {{ ref('stg_shop__orders') }}
),

order_lines as (
    select * from {{ ref('stg_shop__order_lines') }}
),

line_totals as (
    select
        order_id,
        sum(amount) as order_value,
        count(*) as line_count
    from order_lines
    where status != 'cancelled'
    group by order_id
),

final as (
    select
        orders.order_id,
        orders.customer_id,
        orders.ordered_at,
        coalesce(line_totals.order_value, 0) as order_value,
        coalesce(line_totals.line_count, 0) as line_count
    from orders
    left join line_totals using (order_id)
)

select * from final
  • ref() and source() only — never a hard-coded schema.table.
  • Aggregate to the join key before joining (line_totals), so the join stays one-to-one.
  • One transformation per CTE, named for what it holds; explicit column lists in the final select.

3. Window functions without surprises

-- latest row per key, deterministic tie-break
select *
from events
qualify row_number() over (partition by event_id order by loaded_at desc, _row_id desc) = 1

-- running total: say ROWS, or ties on order_date are summed together
sum(amount) over (
    partition by customer_id
    order by order_date, order_id
    rows between unbounded preceding and current row
) as running_spend

-- previous value
lag(order_date) over (partition by customer_id order by order_date, order_id) as previous_order_date

QUALIFY exists in Snowflake, BigQuery, Databricks and DuckDB; elsewhere filter row_number() = 1 in an outer query. A window ORDER BY without a unique tie-breaker gives a different "latest row" on each run.

4. SQL pitfalls checklist

PitfallFix
x not in (select y ...) where y can be NULL returns no rowsnot exists (...)
Integer division (1 / 2 = 0 in Postgres, Redshift, SQL Server)cast one side to numeric/float
Division by zerox / nullif(y, 0)
between on timestamps includes the end instanthalf-open: >= start and < end
Filter on the right table of a LEFT JOIN in WHERE turns it into an inner joinmove the condition into ON
count(col) skips NULLs, count(*) does notchoose deliberately; count(distinct key) for entities through joins
select distinct masking a fan-outfix the join grain instead
Date truncation in local time vs UTCconvert to one timezone first, state which
Average of per-group averagesaggregate from rows, or weight by group size
union silently deduplicatingunion all unless dedup is intended

5. Tests: grain, relationships, contracts

models:
  - name: fct_orders
    config:
      contract:
        enforced: true
    columns:
      - name: order_id
        data_type: varchar
        constraints:
          - type: not_null
        data_tests:
          - unique
          - not_null
      - name: customer_id
        data_type: varchar
        data_tests:
          - relationships:
              to: ref('dim_customers')
              field: customer_id
      - name: order_value
        data_type: numeric
  • Every model: unique + not_null on its key. A composite key uses dbt_utils.unique_combination_of_columns when the repo has dbt_utils, otherwise a surrogate key column with the two tests.
  • Enforced contracts on marts that other teams or dashboards consume: a column rename or type change fails the build instead of the dashboard.
  • Newer dbt versions accept generic-test arguments nested under arguments: — write tests in the form the repository's YAML already uses.

6. dbt unit tests — TDD for SQL (dbt 1.8+)

unit_tests:
  - name: fct_orders_excludes_cancelled_lines
    model: fct_orders
    given:
      - input: ref('stg_shop__orders')
        rows:
          - {order_id: o1, customer_id: c1, ordered_at: '2026-01-05'}
          - {order_id: o2, customer_id: c1, ordered_at: '2026-01-06'}
      - input: ref('stg_shop__order_lines')
        rows:
          - {order_id: o1, amount: 10, status: ok}
          - {order_id: o1, amount: 5, status: cancelled}
    expect:
      rows:
        - {order_id: o1, order_value: 10, line_count: 1}
        - {order_id: o2, order_value: 0, line_count: 0}

Write the unit test first, watch dbt test --select fct_orders fail, then change the SQL. Unit tests need the model's parents to exist as relations; dbt run --select +fct_orders --empty creates them without data in the dev target.

7. Incremental models

{{ config(
    materialized='incremental',
    unique_key='order_id',
    incremental_strategy='merge',
    on_schema_change='append_new_columns'
) }}

select ...
from {{ ref('stg_shop__orders') }}
{% if is_incremental() %}
where updated_at > (select {{ dbt.dateadd('day', -3, 'max(updated_at)') }} from {{ this }})
{% endif %}
  • A unique_key so re-processed rows update instead of duplicating; a lookback window for late-arriving rows.
  • Prove it: build twice in dev and the unique test still passes; a --full-refresh build and an incremental build agree on row counts for the same period.
  • A required full refresh or backfill in production is deploy-time work: record it in before_deploy / after_deploy (rule deploy-runbook-fields).

8. Running it

GoalCommand
Parse without a warehousedbt parse
See the compiled SQLdbt compile --select fct_orders (then read target/compiled/...)
Look at rowsdbt show --select fct_orders --limit 20
Build + test what changed and its childrendbt build --select state:modified+ --state <prod artifacts> (only when the repo keeps state artifacts)
Otherwisedbt build --select fct_orders+ against the dev target

Credentials come from env_var() in profiles.yml, never from the repo. With no warehouse access here, run what you can (dbt parse, a DuckDB target if the project has one) and say exactly which tests did not run.

9. SQL from Python

Bind parameters — never format values into SQL strings:

# ❌ injection and quoting bugs
pd.read_sql(f"select * from orders where country = '{country}'", engine)

# ✅
pd.read_sql(text("select * from orders where country = :country"), engine, params={"country": country})

Common Mistakes

  • Joining before aggregating, then "fixing" the inflated sum with distinct.
  • A model without a key test.
  • Incremental filter with no lookback, silently dropping late rows.
  • Window functions ordered by a non-unique column.
  • Contract enabled with column types that differ by warehouse.

Red Flags

  • Row count of a model larger than its driving table on a one-to-one design.
  • A key test disabled or set to severity: warn without a reason.
  • select * from a source in a mart.
  • Results that change between two runs on unchanged data.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

makifbaysal/tasktrooper1172026년 10월 18일 업데이트

makifbaysal의 스킬 모두 보기

이 스킬의 문제 신고