snowflake-dbt-airbnb-analytics

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Snowflake dbt Airbnb Analytics

Snowflake dbt Airbnb Analytics

Skill by ara.so — Data Skills collection.
This project demonstrates a complete analytics engineering workflow using Snowflake, dbt, and Streamlit. It loads Inside Airbnb open data into Snowflake, transforms it through a layered dbt architecture (staging → intermediate → marts), validates data quality with tests, and serves insights via a Streamlit dashboard.
ara.so提供的技能——数据技能合集。
本项目展示了一个使用Snowflake、dbt和Streamlit的完整分析工程工作流。它将Inside Airbnb开放数据加载到Snowflake中,通过分层dbt架构(staging → intermediate → marts)进行转换,用测试验证数据质量,并通过Streamlit仪表板呈现洞察结果。

What This Project Does

项目功能

  • Raw data ingestion: Loads CSV/GZIP files from Inside Airbnb into Snowflake internal stages
  • Layered transformations: Implements staging (clean/cast), intermediate (joins/enrichment), and mart (dimensions/facts) layers
  • Incremental modeling: Uses Snowflake merge strategy for fact tables
  • Data quality: Generic and singular dbt tests validate uniqueness, relationships, and business rules
  • Analytics dashboard: Streamlit app queries marts for neighbourhood and listing performance
Data sources:
listings.csv.gz
,
calendar.csv.gz
,
reviews.csv.gz
,
neighbourhoods.csv
from Inside Airbnb
  • 原始数据摄入:将Inside Airbnb的CSV/GZIP文件加载到Snowflake内部阶段
  • 分层转换:实现staging(清洗/类型转换)、intermediate(关联/增强)和mart(维度/事实)层
  • 增量建模:对事实表使用Snowflake合并策略
  • 数据质量:通过通用和自定义dbt测试验证唯一性、关联关系和业务规则
  • 分析仪表板:Streamlit应用查询mart层数据,展示社区和房源的表现
数据源:来自Inside Airbnb
listings.csv.gz
calendar.csv.gz
reviews.csv.gz
neighbourhoods.csv

Installation

安装步骤

bash
undefined
bash
undefined

Clone and set up environment

克隆项目并设置环境

git clone https://github.com/analyticsdurgesh/Snowflake_DBT_Project.git cd Snowflake_DBT_Project python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt
undefined
git clone https://github.com/analyticsdurgesh/Snowflake_DBT_Project.git cd Snowflake_DBT_Project python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt
undefined

Configuration

配置说明

1. Snowflake Credentials

1. Snowflake凭证

Create local-only credential files (ignored by git):
bash
cp profiles.yml.example profiles.yml
cp config/local_credentials.example.json config/local_credentials.json
profiles.yml
(dbt connection):
yaml
airbnb_snowflake:
  target: dev
  outputs:
    dev:
      type: snowflake
      account: YOUR_ACCOUNT
      user: YOUR_USER
      password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
      role: YOUR_ROLE
      database: AIRBNB_DB
      warehouse: COMPUTE_WH
      schema: ANALYTICS
      threads: 4
      client_session_keep_alive: False
config/local_credentials.json
(Streamlit connection):
json
{
  "account": "YOUR_ACCOUNT",
  "user": "YOUR_USER",
  "password": "YOUR_PASSWORD",
  "role": "YOUR_ROLE",
  "warehouse": "COMPUTE_WH",
  "database": "AIRBNB_DB",
  "schema": "ANALYTICS"
}
Use environment variables in production:
bash
export SNOWFLAKE_PASSWORD="your_password"
创建本地凭证文件(会被git忽略):
bash
cp profiles.yml.example profiles.yml
cp config/local_credentials.example.json config/local_credentials.json
profiles.yml
(dbt连接配置):
yaml
airbnb_snowflake:
  target: dev
  outputs:
    dev:
      type: snowflake
      account: YOUR_ACCOUNT
      user: YOUR_USER
      password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
      role: YOUR_ROLE
      database: AIRBNB_DB
      warehouse: COMPUTE_WH
      schema: ANALYTICS
      threads: 4
      client_session_keep_alive: False
config/local_credentials.json
(Streamlit连接配置):
json
{
  "account": "YOUR_ACCOUNT",
  "user": "YOUR_USER",
  "password": "YOUR_PASSWORD",
  "role": "YOUR_ROLE",
  "warehouse": "COMPUTE_WH",
  "database": "AIRBNB_DB",
  "schema": "ANALYTICS"
}
生产环境使用环境变量:
bash
export SNOWFLAKE_PASSWORD="your_password"

2. Download Inside Airbnb Data

2. 下载Inside Airbnb数据

Place raw files in
data/raw/
:
text
data/raw/listings.csv.gz
data/raw/calendar.csv.gz
data/raw/reviews.csv.gz
data/raw/neighbourhoods.csv
Recommended dataset: New York City from Inside Airbnb.
将原始文件放置在
data/raw/
目录下:
text
data/raw/listings.csv.gz
data/raw/calendar.csv.gz
data/raw/reviews.csv.gz
data/raw/neighbourhoods.csv
推荐数据集:来自Inside Airbnb的纽约市数据。

Loading Raw Data

加载原始数据

The Python loader creates Snowflake objects and stages data:
bash
python scripts/load_inside_airbnb_to_snowflake.py
What it does:
  1. Executes
    setup/snowflake_setup.sql
    to create database, schemas, and stage
  2. Uploads raw files to
    INSIDE_AIRBNB_STAGE
  3. Creates raw tables with headers from CSV files
  4. Copies staged data into
    RAW
    schema tables
Key loader code patterns:
python
import snowflake.connector
import json
Python加载器会创建Snowflake对象并将数据上传到阶段:
bash
python scripts/load_inside_airbnb_to_snowflake.py
执行流程
  1. 执行
    setup/snowflake_setup.sql
    创建数据库、模式和阶段
  2. 将原始文件上传到
    INSIDE_AIRBNB_STAGE
  3. 根据CSV文件表头创建原始表
  4. 将阶段中的数据复制到
    RAW
    模式的表中
加载器核心代码示例
python
import snowflake.connector
import json

Load credentials

加载凭证

with open('config/local_credentials.json') as f: creds = json.load(f)
with open('config/local_credentials.json') as f: creds = json.load(f)

Connect to Snowflake

连接Snowflake

conn = snowflake.connector.connect( account=creds['account'], user=creds['user'], password=creds['password'], role=creds['role'], warehouse=creds['warehouse'] )
conn = snowflake.connector.connect( account=creds['account'], user=creds['user'], password=creds['password'], role=creds['role'], warehouse=creds['warehouse'] )

Upload to stage

上传到阶段

conn.cursor().execute(f"PUT file://data/raw/listings.csv.gz @INSIDE_AIRBNB_STAGE")
conn.cursor().execute(f"PUT file://data/raw/listings.csv.gz @INSIDE_AIRBNB_STAGE")

Copy into raw table

复制到原始表

conn.cursor().execute(""" COPY INTO RAW.LISTINGS FROM @INSIDE_AIRBNB_STAGE/listings.csv.gz FILE_FORMAT = (TYPE = 'CSV' SKIP_HEADER = 1 FIELD_OPTIONALLY_ENCLOSED_BY = '"') """)
undefined
conn.cursor().execute(""" COPY INTO RAW.LISTINGS FROM @INSIDE_AIRBNB_STAGE/listings.csv.gz FILE_FORMAT = (TYPE = 'CSV' SKIP_HEADER = 1 FIELD_OPTIONALLY_ENCLOSED_BY = '"') """)
undefined

dbt Model Architecture

dbt模型架构

Layer Structure

分层结构

LayerPathPurposeExample
Sources
models/staging/sources.yml
Define raw tables
RAW.LISTINGS
Staging
models/staging/stg_*.sql
Clean, cast, standardize
stg_airbnb__listings
Intermediate
models/intermediate/int_*.sql
Joins, enrichment, business logic
int_airbnb__listing_enriched
Marts
models/marts/
Dimensions, facts, aggregates
dim_listings
,
fct_listing_calendar
层级路径用途示例
Sources
models/staging/sources.yml
定义原始表
RAW.LISTINGS
Staging
models/staging/stg_*.sql
清洗、类型转换、标准化
stg_airbnb__listings
Intermediate
models/intermediate/int_*.sql
关联、增强、业务逻辑实现
int_airbnb__listing_enriched
Marts
models/marts/
维度表、事实表、聚合表
dim_listings
,
fct_listing_calendar

Staging Layer Example

Staging层示例

models/staging/stg_airbnb__listings.sql
:
sql
with source as (
    select * from {{ source('airbnb_raw', 'listings') }}
),

cleaned as (
    select
        id::bigint as listing_id,
        name::varchar as listing_name,
        host_id::bigint as host_id,
        host_name::varchar as host_name,
        neighbourhood_cleansed::varchar as neighbourhood,
        room_type::varchar as room_type,
        price::varchar as price_raw,
        minimum_nights::int as minimum_nights,
        number_of_reviews::int as number_of_reviews,
        last_review::date as last_review_date,
        reviews_per_month::float as reviews_per_month,
        availability_365::int as availability_365
    from source
)

select * from cleaned
Key patterns:
  • Use
    {{ source() }}
    for raw table references
  • Cast types explicitly with
    ::
  • Standardize column names (snake_case)
  • Preserve raw columns when cleaning needed downstream
models/staging/stg_airbnb__listings.sql
:
sql
with source as (
    select * from {{ source('airbnb_raw', 'listings') }}
),

cleaned as (
    select
        id::bigint as listing_id,
        name::varchar as listing_name,
        host_id::bigint as host_id,
        host_name::varchar as host_name,
        neighbourhood_cleansed::varchar as neighbourhood,
        room_type::varchar as room_type,
        price::varchar as price_raw,
        minimum_nights::int as minimum_nights,
        number_of_reviews::int as number_of_reviews,
        last_review::date as last_review_date,
        reviews_per_month::float as reviews_per_month,
        availability_365::int as availability_365
    from source
)

select * from cleaned
核心模式:
  • 使用
    {{ source() }}
    引用原始表
  • ::
    显式转换数据类型
  • 标准化列名(蛇形命名法)
  • 清洗时保留后续可能需要的原始列

Intermediate Layer Example

Intermediate层示例

models/intermediate/int_airbnb__calendar_enriched.sql
:
sql
with calendar as (
    select * from {{ ref('stg_airbnb__calendar') }}
),

listings as (
    select * from {{ ref('int_airbnb__listing_enriched') }}
),

enriched as (
    select
        c.listing_id,
        c.calendar_date,
        c.available,
        c.price,
        c.adjusted_price,
        c.minimum_nights,
        c.maximum_nights,
        l.listing_name,
        l.neighbourhood,
        l.room_type,
        l.host_id,
        l.host_name,
        -- Revenue proxy: price when unavailable
        case
            when c.available = false and c.price > 0
            then c.price
            else 0
        end as estimated_revenue
    from calendar c
    left join listings l
        on c.listing_id = l.listing_id
)

select * from enriched
Key patterns:
  • Use
    {{ ref() }}
    for model dependencies
  • Join staging/intermediate models
  • Add calculated business logic (revenue proxy)
  • Keep intermediate models focused on reusable logic
models/intermediate/int_airbnb__calendar_enriched.sql
:
sql
with calendar as (
    select * from {{ ref('stg_airbnb__calendar') }}
),

listings as (
    select * from {{ ref('int_airbnb__listing_enriched') }}
),

enriched as (
    select
        c.listing_id,
        c.calendar_date,
        c.available,
        c.price,
        c.adjusted_price,
        c.minimum_nights,
        c.maximum_nights,
        l.listing_name,
        l.neighbourhood,
        l.room_type,
        l.host_id,
        l.host_name,
        -- 收入估算:不可预订时的价格
        case
            when c.available = false and c.price > 0
            then c.price
            else 0
        end as estimated_revenue
    from calendar c
    left join listings l
        on c.listing_id = l.listing_id
)

select * from enriched
核心模式:
  • 使用
    {{ ref() }}
    引用模型依赖
  • 关联staging/intermediate层模型
  • 添加计算型业务逻辑(收入估算)
  • 保持intermediate模型专注于可复用逻辑

Incremental Fact Table Example

增量事实表示例

models/marts/fct_listing_calendar.sql
:
sql
{{
    config(
        materialized='incremental',
        unique_key=['listing_id', 'calendar_date'],
        merge_update_columns=['available', 'price', 'estimated_revenue']
    )
}}

with calendar_enriched as (
    select * from {{ ref('int_airbnb__calendar_enriched') }}
)

select
    listing_id,
    calendar_date,
    available,
    price,
    adjusted_price,
    minimum_nights,
    maximum_nights,
    neighbourhood,
    room_type,
    host_id,
    estimated_revenue
from calendar_enriched

{% if is_incremental() %}
    where calendar_date > (select max(calendar_date) from {{ this }})
{% endif %}
Key patterns:
  • materialized='incremental'
    for large fact tables
  • unique_key
    for merge strategy (update existing, insert new)
  • merge_update_columns
    specifies which columns to update
  • is_incremental()
    filters new records only on subsequent runs
  • Use
    --full-refresh
    flag to rebuild from scratch
models/marts/fct_listing_calendar.sql
:
sql
{{
    config(
        materialized='incremental',
        unique_key=['listing_id', 'calendar_date'],
        merge_update_columns=['available', 'price', 'estimated_revenue']
    )
}}

with calendar_enriched as (
    select * from {{ ref('int_airbnb__calendar_enriched') }}
)

select
    listing_id,
    calendar_date,
    available,
    price,
    adjusted_price,
    minimum_nights,
    maximum_nights,
    neighbourhood,
    room_type,
    host_id,
    estimated_revenue
from calendar_enriched

{% if is_incremental() %}
    where calendar_date > (select max(calendar_date) from {{ this }})
{% endif %}
核心模式:
  • 对大型事实表使用
    materialized='incremental'
  • unique_key
    用于合并策略(更新现有数据,插入新数据)
  • merge_update_columns
    指定需要更新的列
  • is_incremental()
    在后续运行时仅过滤新记录
  • 使用
    --full-refresh
    标志从头重建表

Aggregate Mart Example

聚合Mart示例

models/marts/agg_neighbourhood_monthly_performance.sql
:
sql
with listing_monthly as (
    select * from {{ ref('agg_listing_monthly_performance') }}
)

select
    neighbourhood,
    year_month,
    count(distinct listing_id) as total_listings,
    sum(total_days) as total_days,
    sum(available_days) as total_available_days,
    sum(unavailable_days) as total_unavailable_days,
    round(avg(availability_rate), 2) as avg_availability_rate,
    round(sum(estimated_revenue), 2) as total_estimated_revenue,
    round(avg(avg_price), 2) as avg_listing_price
from listing_monthly
group by neighbourhood, year_month
order by neighbourhood, year_month
Key patterns:
  • Aggregate from lower-level marts
  • Use
    round()
    for clean reporting metrics
  • Group by dimensions for dashboards
models/marts/agg_neighbourhood_monthly_performance.sql
:
sql
with listing_monthly as (
    select * from {{ ref('agg_listing_monthly_performance') }}
)

select
    neighbourhood,
    year_month,
    count(distinct listing_id) as total_listings,
    sum(total_days) as total_days,
    sum(available_days) as total_available_days,
    sum(unavailable_days) as total_unavailable_days,
    round(avg(availability_rate), 2) as avg_availability_rate,
    round(sum(estimated_revenue), 2) as total_estimated_revenue,
    round(avg(avg_price), 2) as avg_listing_price
from listing_monthly
group by neighbourhood, year_month
order by neighbourhood, year_month
核心模式:
  • 从低层级mart表聚合数据
  • 使用
    round()
    生成整洁的报告指标
  • 按维度分组以支持仪表板展示

dbt Commands

dbt命令

bash
undefined
bash
undefined

Test connection

测试连接

dbt debug --profiles-dir .
dbt debug --profiles-dir .

Run all models

运行所有模型

dbt run --profiles-dir .
dbt run --profiles-dir .

Run specific model and downstream dependencies

运行指定模型及其下游依赖

dbt run --select dim_listings+ --profiles-dir .
dbt run --select dim_listings+ --profiles-dir .

Run incremental models with full refresh

全量刷新运行增量模型

dbt run --full-refresh --select fct_listing_calendar+ --profiles-dir .
dbt run --full-refresh --select fct_listing_calendar+ --profiles-dir .

Run tests

运行测试

dbt test --profiles-dir .
dbt test --profiles-dir .

Test specific model

测试指定模型

dbt test --select stg_airbnb__listings --profiles-dir .
dbt test --select stg_airbnb__listings --profiles-dir .

Generate and serve documentation

生成并启动文档服务

dbt docs generate --profiles-dir . dbt docs serve --profiles-dir .

**Common workflows**:

```bash
dbt docs generate --profiles-dir . dbt docs serve --profiles-dir .

**常见工作流**:

```bash

New data load workflow

新数据加载工作流

python scripts/load_inside_airbnb_to_snowflake.py dbt run --full-refresh --select fct_listing_calendar+ --profiles-dir . dbt test --profiles-dir .
python scripts/load_inside_airbnb_to_snowflake.py dbt run --full-refresh --select fct_listing_calendar+ --profiles-dir . dbt test --profiles-dir .

Development workflow (iterative)

开发工作流(迭代式)

dbt run --select +fct_reviews --profiles-dir . # Run model and upstream deps dbt test --select fct_reviews --profiles-dir .
undefined
dbt run --select +fct_reviews --profiles-dir . # 运行模型及其上游依赖 dbt test --select fct_reviews --profiles-dir .
undefined

Data Quality Tests

数据质量测试

Generic Tests in Schema Files

Schema文件中的通用测试

models/staging/schema.yml
:
yaml
version: 2

models:
  - name: stg_airbnb__listings
    columns:
      - name: listing_id
        tests:
          - unique
          - not_null
      - name: room_type
        tests:
          - accepted_values:
              values: ['Entire home/apt', 'Private room', 'Shared room', 'Hotel room']
      - name: price
        tests:
          - not_null
          - dbt_utils.expression_is_true:
              expression: ">= 0"

  - name: stg_airbnb__calendar
    columns:
      - name: listing_id
        tests:
          - relationships:
              to: ref('stg_airbnb__listings')
              field: listing_id
models/staging/schema.yml
:
yaml
version: 2

models:
  - name: stg_airbnb__listings
    columns:
      - name: listing_id
        tests:
          - unique
          - not_null
      - name: room_type
        tests:
          - accepted_values:
              values: ['Entire home/apt', 'Private room', 'Shared room', 'Hotel room']
      - name: price
        tests:
          - not_null
          - dbt_utils.expression_is_true:
              expression: ">= 0"

  - name: stg_airbnb__calendar
    columns:
      - name: listing_id
        tests:
          - relationships:
              to: ref('stg_airbnb__listings')
              field: listing_id

Singular Tests

自定义测试

tests/no_duplicate_listing_dates.sql
:
sql
-- Test for duplicate listing-date combinations in fact table
select
    listing_id,
    calendar_date,
    count(*) as record_count
from {{ ref('fct_listing_calendar') }}
group by listing_id, calendar_date
having count(*) > 1
Key patterns:
  • Generic tests in
    schema.yml
    for standard validations
  • Singular tests in
    tests/
    for custom business rules
  • Tests return records that FAIL the condition
  • Use
    dbt_utils
    package for advanced tests
Install dbt packages (
packages.yml
):
yaml
packages:
  - package: dbt-labs/dbt_utils
    version: 1.1.1
bash
dbt deps --profiles-dir .
tests/no_duplicate_listing_dates.sql
:
sql
-- 测试事实表中是否存在重复的房源-日期组合
select
    listing_id,
    calendar_date,
    count(*) as record_count
from {{ ref('fct_listing_calendar') }}
group by listing_id, calendar_date
having count(*) > 1
核心模式:
  • schema.yml
    中使用通用测试进行标准验证
  • tests/
    目录中使用自定义测试实现业务规则校验
  • 测试返回不符合条件的记录
  • 使用
    dbt_utils
    包实现高级测试
安装dbt包
packages.yml
):
yaml
packages:
  - package: dbt-labs/dbt_utils
    version: 1.1.1
bash
dbt deps --profiles-dir .

Streamlit Dashboard

Streamlit仪表板

dashboard/streamlit_app.py
:
python
import streamlit as st
import snowflake.connector
import pandas as pd
import json
dashboard/streamlit_app.py
:
python
import streamlit as st
import snowflake.connector
import pandas as pd
import json

Load credentials

加载凭证

with open('config/local_credentials.json') as f: creds = json.load(f)
@st.cache_resource def get_connection(): return snowflake.connector.connect( account=creds['account'], user=creds['user'], password=creds['password'], role=creds['role'], warehouse=creds['warehouse'], database=creds['database'], schema=creds['schema'] )
def run_query(query): conn = get_connection() return pd.read_sql(query, conn)
st.title("Inside Airbnb Analytics Dashboard")
with open('config/local_credentials.json') as f: creds = json.load(f)
@st.cache_resource def get_connection(): return snowflake.connector.connect( account=creds['account'], user=creds['user'], password=creds['password'], role=creds['role'], warehouse=creds['warehouse'], database=creds['database'], schema=creds['schema'] )
def run_query(query): conn = get_connection() return pd.read_sql(query, conn)
st.title("Inside Airbnb Analytics Dashboard")

Neighbourhood performance

社区表现

st.header("Top Neighbourhoods by Estimated Revenue") query = """ SELECT neighbourhood, total_estimated_revenue, avg_availability_rate, total_listings FROM agg_neighbourhood_monthly_performance WHERE year_month = (SELECT MAX(year_month) FROM agg_neighbourhood_monthly_performance) ORDER BY total_estimated_revenue DESC LIMIT 10 """ df = run_query(query) st.dataframe(df) st.bar_chart(df.set_index('NEIGHBOURHOOD')['TOTAL_ESTIMATED_REVENUE'])
st.header("按估算收入排序的Top社区") query = """ SELECT neighbourhood, total_estimated_revenue, avg_availability_rate, total_listings FROM agg_neighbourhood_monthly_performance WHERE year_month = (SELECT MAX(year_month) FROM agg_neighbourhood_monthly_performance) ORDER BY total_estimated_revenue DESC LIMIT 10 """ df = run_query(query) st.dataframe(df) st.bar_chart(df.set_index('NEIGHBOURHOOD')['TOTAL_ESTIMATED_REVENUE'])

Room type pricing

房源类型定价

st.header("Average Price by Room Type") query = """ SELECT room_type, ROUND(AVG(price), 2) as avg_price FROM dim_listings WHERE price > 0 GROUP BY room_type ORDER BY avg_price DESC """ df = run_query(query) st.bar_chart(df.set_index('ROOM_TYPE')['AVG_PRICE'])

**Run dashboard**:

```bash
streamlit run dashboard/streamlit_app.py
Key patterns:
  • Use
    @st.cache_resource
    for connection pooling
  • Query marts directly for performance
  • Filter to latest snapshot with
    MAX(year_month)
  • Keep credentials in separate JSON file
st.header("按房源类型划分的平均价格") query = """ SELECT room_type, ROUND(AVG(price), 2) as avg_price FROM dim_listings WHERE price > 0 GROUP BY room_type ORDER BY avg_price DESC """ df = run_query(query) st.bar_chart(df.set_index('ROOM_TYPE')['AVG_PRICE'])

**启动仪表板**:

```bash
streamlit run dashboard/streamlit_app.py
核心模式:
  • 使用
    @st.cache_resource
    实现连接池
  • 直接查询mart层以提升性能
  • 使用
    MAX(year_month)
    过滤最新数据快照
  • 将凭证保存在独立的JSON文件中

Common Patterns

通用模式

Adding a New Staging Model

添加新的Staging模型

  1. Define source in
    models/staging/sources.yml
    :
yaml
sources:
  - name: airbnb_raw
    database: AIRBNB_DB
    schema: RAW
    tables:
      - name: new_table
  1. Create staging model
    models/staging/stg_airbnb__new_table.sql
    :
sql
with source as (
    select * from {{ source('airbnb_raw', 'new_table') }}
),

cleaned as (
    select
        id::bigint as record_id,
        field::varchar as clean_field
    from source
)

select * from cleaned
  1. Add tests in
    models/staging/schema.yml
    :
yaml
models:
  - name: stg_airbnb__new_table
    columns:
      - name: record_id
        tests:
          - unique
          - not_null
  1. models/staging/sources.yml
    中定义数据源:
yaml
sources:
  - name: airbnb_raw
    database: AIRBNB_DB
    schema: RAW
    tables:
      - name: new_table
  1. 创建Staging模型
    models/staging/stg_airbnb__new_table.sql
    :
sql
with source as (
    select * from {{ source('airbnb_raw', 'new_table') }}
),

cleaned as (
    select
        id::bigint as record_id,
        field::varchar as clean_field
    from source
)

select * from cleaned
  1. models/staging/schema.yml
    中添加测试:
yaml
models:
  - name: stg_airbnb__new_table
    columns:
      - name: record_id
        tests:
          - unique
          - not_null

Creating a Dimension Table

创建维度表

models/marts/dim_hosts.sql
:
sql
with listings as (
    select * from {{ ref('int_airbnb__listing_enriched') }}
),

host_agg as (
    select
        host_id,
        max(host_name) as host_name,
        count(*) as total_listings,
        round(avg(price), 2) as avg_listing_price,
        sum(number_of_reviews) as total_reviews
    from listings
    group by host_id
)

select * from host_agg
Key patterns:
  • Aggregate from enriched intermediate layer
  • Use
    max()
    to select representative values
  • Include business metrics (counts, averages)
models/marts/dim_hosts.sql
:
sql
with listings as (
    select * from {{ ref('int_airbnb__listing_enriched') }}
),

host_agg as (
    select
        host_id,
        max(host_name) as host_name,
        count(*) as total_listings,
        round(avg(price), 2) as avg_listing_price,
        sum(number_of_reviews) as total_reviews
    from listings
    group by host_id
)

select * from host_agg
核心模式:
  • 从增强后的intermediate层聚合数据
  • 使用
    max()
    选择代表性值
  • 包含业务指标(计数、平均值)

Monthly Aggregation Pattern

月度聚合模式

sql
with daily_facts as (
    select * from {{ ref('fct_listing_calendar') }}
)

select
    listing_id,
    to_char(calendar_date, 'YYYY-MM') as year_month,
    count(*) as total_days,
    sum(case when available then 1 else 0 end) as available_days,
    sum(case when not available then 1 else 0 end) as unavailable_days,
    round(avg(case when available then 1.0 else 0.0 end), 2) as availability_rate,
    round(sum(estimated_revenue), 2) as estimated_revenue,
    round(avg(price), 2) as avg_price
from daily_facts
group by listing_id, to_char(calendar_date, 'YYYY-MM')
Key patterns:
  • Use
    to_char(date, 'YYYY-MM')
    for month grouping in Snowflake
  • Calculate rates with
    avg(case when condition then 1.0 else 0.0 end)
  • Aggregate revenue as sum, prices as average
sql
with daily_facts as (
    select * from {{ ref('fct_listing_calendar') }}
)

select
    listing_id,
    to_char(calendar_date, 'YYYY-MM') as year_month,
    count(*) as total_days,
    sum(case when available then 1 else 0 end) as available_days,
    sum(case when not available then 1 else 0 end) as unavailable_days,
    round(avg(case when available then 1.0 else 0.0 end), 2) as availability_rate,
    round(sum(estimated_revenue), 2) as estimated_revenue,
    round(avg(price), 2) as avg_price
from daily_facts
group by listing_id, to_char(calendar_date, 'YYYY-MM')
核心模式:
  • 在Snowflake中使用
    to_char(date, 'YYYY-MM')
    进行月度分组
  • 使用
    avg(case when condition then 1.0 else 0.0 end)
    计算比率
  • 收入按总和聚合,价格按平均值聚合

Troubleshooting

故障排除

dbt Connection Issues

dbt连接问题

Error:
Database Error in model [...] (...)  250001 (08001): Failed to connect to DB
Solution:
  1. Verify
    profiles.yml
    has correct Snowflake account identifier
  2. Test connection:
    dbt debug --profiles-dir .
  3. Check Snowflake credentials and network access
  4. Ensure warehouse is running
错误:
Database Error in model [...] (...)  250001 (08001): Failed to connect to DB
解决方案:
  1. 验证
    profiles.yml
    中的Snowflake账户标识符是否正确
  2. 测试连接:
    dbt debug --profiles-dir .
  3. 检查Snowflake凭证和网络访问权限
  4. 确保计算仓库处于运行状态

Incremental Model Not Updating

增量模型未更新

Error: New data not appearing in incremental fact table
Solution:
bash
undefined
错误: 新数据未出现在增量事实表中
解决方案:
bash
undefined

Force full rebuild

强制全量重建

dbt run --full-refresh --select fct_listing_calendar --profiles-dir .

Check `unique_key` matches grain in config:
```sql
{{
    config(
        unique_key=['listing_id', 'calendar_date']  -- Must match table grain
    )
}}
dbt run --full-refresh --select fct_listing_calendar --profiles-dir .

检查配置中的`unique_key`是否与表粒度匹配:
```sql
{{
    config(
        unique_key=['listing_id', 'calendar_date']  -- 必须与表粒度匹配
    )
}}

Test Failures on Price Data

价格数据测试失败

Error:
dbt_utils.expression_is_true
fails on price column
Solution: Raw price data may contain non-numeric values or currency symbols.
Clean in staging layer:
sql
-- Remove $ and commas, cast to numeric
replace(replace(price, '$', ''), ',', '')::decimal(10,2) as price
错误:
dbt_utils.expression_is_true
在价格列上测试失败
解决方案: 原始价格数据可能包含非数值或货币符号。
在Staging层进行清洗:
sql
-- 移除$和逗号,转换为数值类型
replace(replace(price, '$', ''), ',', '')::decimal(10,2) as price

Streamlit Connection Timeout

Streamlit连接超时

Error:
OperationalError: 250001 (08001): Failed to connect
Solution:
  1. Check
    config/local_credentials.json
    credentials
  2. Verify Snowflake warehouse is running
  3. Add timeout config:
python
conn = snowflake.connector.connect(
    ...,
    login_timeout=30,
    network_timeout=30
)
错误:
OperationalError: 250001 (08001): Failed to connect
解决方案:
  1. 检查
    config/local_credentials.json
    中的凭证
  2. 验证Snowflake计算仓库是否运行
  3. 添加超时配置:
python
conn = snowflake.connector.connect(
    ...,
    login_timeout=30,
    network_timeout=30
)

Missing Stage Files

阶段文件缺失

Error:
File not found
when running loader script
Solution:
  1. Verify raw files exist in
    data/raw/
  2. Check file names match loader script expectations
  3. Ensure files are compressed (
    .gz
    ) where expected
错误: 运行加载器脚本时出现
File not found
解决方案:
  1. 验证原始文件是否存在于
    data/raw/
    目录
  2. 检查文件名是否与加载器脚本预期一致
  3. 确保文件按预期压缩(
    .gz
    格式)

dbt Model Dependency Errors

dbt模型依赖错误

Error:
Compilation Error: Model 'X' depends on a node named 'Y' which was not found
Solution:
  1. Check
    {{ ref('model_name') }}
    matches actual model file name
  2. Run
    dbt deps --profiles-dir .
    to install packages
  3. Verify model exists in
    models/
    directory
错误:
Compilation Error: Model 'X' depends on a node named 'Y' which was not found
解决方案:
  1. 检查
    {{ ref('model_name') }}
    是否与实际模型文件名匹配
  2. 运行
    dbt deps --profiles-dir .
    安装依赖包
  3. 验证模型是否存在于
    models/
    目录

Environment Variables for Production

生产环境环境变量

Use environment variables instead of local credential files:
dbt
profiles.yml
:
yaml
airbnb_snowflake:
  target: prod
  outputs:
    prod:
      type: snowflake
      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
      user: "{{ env_var('SNOWFLAKE_USER') }}"
      password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
      role: "{{ env_var('SNOWFLAKE_ROLE') }}"
      database: "{{ env_var('SNOWFLAKE_DATABASE') }}"
      warehouse: "{{ env_var('SNOWFLAKE_WAREHOUSE') }}"
      schema: ANALYTICS
      threads: 4
Streamlit connection:
python
import os

conn = snowflake.connector.connect(
    account=os.getenv('SNOWFLAKE_ACCOUNT'),
    user=os.getenv('SNOWFLAKE_USER'),
    password=os.getenv('SNOWFLAKE_PASSWORD'),
    role=os.getenv('SNOWFLAKE_ROLE'),
    warehouse=os.getenv('SNOWFLAKE_WAREHOUSE'),
    database=os.getenv('SNOWFLAKE_DATABASE'),
    schema='ANALYTICS'
)
使用环境变量替代本地凭证文件:
dbt
profiles.yml
:
yaml
airbnb_snowflake:
  target: prod
  outputs:
    prod:
      type: snowflake
      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
      user: "{{ env_var('SNOWFLAKE_USER') }}"
      password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
      role: "{{ env_var('SNOWFLAKE_ROLE') }}"
      database: "{{ env_var('SNOWFLAKE_DATABASE') }}"
      warehouse: "{{ env_var('SNOWFLAKE_WAREHOUSE') }}"
      schema: ANALYTICS
      threads: 4
Streamlit连接:
python
import os

conn = snowflake.connector.connect(
    account=os.getenv('SNOWFLAKE_ACCOUNT'),
    user=os.getenv('SNOWFLAKE_USER'),
    password=os.getenv('SNOWFLAKE_PASSWORD'),
    role=os.getenv('SNOWFLAKE_ROLE'),
    warehouse=os.getenv('SNOWFLAKE_WAREHOUSE'),
    database=os.getenv('SNOWFLAKE_DATABASE'),
    schema='ANALYTICS'
)

Project Resources

项目资源