zalo-icon
facebook-icon
phone-icon
Sources, ref() và DAG trong dbt: Quản lý phụ thuộc và chọn model | Bài 4/10

Series dbt · Bài 4/10. Bạn sửa stg_orders rồi muốn cập nhật báo cáo liên quan, nhưng không muốn chạy toàn bộ project. Làm sao biết model nào cần chạy trước, model nào bị ảnh hưởng và câu SQL thực sự trỏ vào schema nào? source(), ref() và graph selection giúp trả lời những câu hỏi này.

Bài tiếp tục project dbt-retail sau bài 3. Cần có đủ bốn staging model, int_order_amounts, int_orders_enriched và mart_daily_revenue; dữ liệu đã hoàn nguyên về 3 đơn paid với tổng 400.000 VND. Lab vẫn dùng dbt Core 1.9.8 và adapter PostgreSQL đã cài, không chuyển sang hướng dẫn runtime mới khác.

1. Source: biết rõ đang đọc cái gì

Trong _sources.yml, name: shop là tên logic của nhóm nguồn; schema: raw là tên schema vật lý. Bảng orders được tham chiếu bằng source(shop, orders). Nếu nhà cung cấp đổi tên schema vật lý, bạn có một điểm cấu hình tập trung thay vì sửa hàng chục câu SQL đã hard-code raw.orders. Điều đó chỉ đúng khi các model thực sự sử dụng source, không bỏ qua nó bằng tên bảng trực tiếp.

version: 2
sources:
  - name: shop
    description: 'Nguon ban hang gia lap cho series dbt'
    schema: raw
    tables:
      - name: customers
        description: 'Mot dong moi khach hang'
      - name: products
        description: 'Mot dong moi san pham'
      - name: orders
        description: 'Mot dong moi don; chua loc paid'
      - name: order_items
        description: 'Mot dong moi dong hang cua don'

Có thể thay models/staging/_sources.yml bằng bản này; nó giữ nguyên bốn source và thêm mô tả. Khai báo source không tải dữ liệu, không tự tạo quyền truy cập và không chứng minh bảng đã cập nhật đúng giờ. Freshness cần trường thời gian và cấu hình phù hợp; dòng mô tả “dữ liệu mới” không biến thành kiểm tra tự động.

Nguồn lab chỉ có ngày giao dịch, không có thời điểm ingestion. Không nên dùng ordered_at làm loaded_at_field để đo ingestion freshness vì một đơn cũ được nạp hôm nay vẫn có ngày giao dịch cũ. Bài này tập trung dependency; chỉ thêm freshness khi hiểu đúng đồng hồ bạn muốn đo.

Source ghi nhận nguồn dữ liệu đầu vào và nơi đọc dữ liệu.
Source ghi nhận nguồn dữ liệu đầu vào và nơi đọc dữ liệu.

2. ref không chỉ là cách viết tên bảng đẹp hơn

select
    order_id,
    sum(line_amount_vnd) as order_amount_vnd
from {{ ref('stg_order_items') }}
group by order_id

Đây là đoạn minh họa, không thay file int_order_amounts đầy đủ của bài 3 vì model đó còn line_count và total_quantity được dùng phía sau. ref trả về relation theo môi trường và ghi nhận một cạnh phụ thuộc trong graph: model hiện tại cần stg_order_items. Viết FROM dev_staging.stg_order_items trực tiếp có thể đọc được bảng nhưng không khai báo quan hệ này cho dbt theo cùng cách.

Cách viếtDatabase có thể đọc?dbt có dependency khai báo?
{{ ref('stg_order_items') }}Sau khi được compile và relation tồn tạiCó, với model/seed/snapshot tương ứng
{{ source('shop', 'order_items') }}Sau khi compile và bảng nguồn tồn tạiCó, với source đã khai báo
dev_staging.stg_order_itemsCó nếu schema và bảng tồn tạiKhông tự trở thành ref chỉ vì tên giống model

Tên file stg_order_items.sql tạo tên model mặc định stg_order_items; ref dùng tên model, không có đuôi .sql. Alias có thể thay tên relation vật lý mà không đổi tên model được dùng trong ref. Phân biệt ba tên: file/model, alias và schema sẽ giúp bạn sửa lỗi “relation not found” nhanh hơn.

3. Đọc graph của project trước khi chạy

Graph của báo cáo ngày có một nhánh nguồn orders đi qua stg_orders, và một nhánh order_items đi qua stg_order_items rồi int_order_amounts. Hai nhánh gặp ở int_orders_enriched, sau đó mart_daily_revenue tổng hợp theo ngày. stg_customers và stg_products đã có trong project nhưng báo cáo ngày hiện chưa cần chúng.

dbt ls --profiles-dir profiles --resource-type model
dbt ls --profiles-dir profiles --select +mart_daily_revenue --resource-type model
dbt ls --profiles-dir profiles --select source:shop.orders+ --resource-type model

Dùng dbt ls để xem phạm vi chọn trước khi chạy. Lệnh cuối lấy các model downstream của source shop.orders. Bảng source không được xây bằng dbt run; nó chỉ nằm trong graph như đầu vào. Thứ tự dòng của dbt ls không phải lịch thực thi mà bạn nên tự dùng để chạy từng file. dbt run lập thứ tự theo dependency và mức song song cho phép.

DAG phải không có vòng. Nếu A ref B và B ref A, không có model nào làm đầu vào trước để xây hợp lệ. Sửa bằng cách xem lại trách nhiệm model hoặc đưa logic chung về upstream, không phải đổi tên file hay giảm threads để tránh vòng lặp.

Dependency xác định phần nào phải có trước khi xây phần tiếp theo.
Dependency xác định phần nào phải có trước khi xây phần tiếp theo.

4. Chọn đúng phạm vi: dấu + trước và sau

SelectorÝ nghĩaDùng khi
mart_daily_revenueChỉ model được chọnUpstream đã tồn tại và đủ mới
+mart_daily_revenueModel cùng tất cả ancestorsMuốn xây đủ chuỗi upstream cần thiết
stg_orders+Model cùng downstreamThay đổi logic orders và cập nhật model chịu ảnh hưởng
+int_orders_enriched+Upstream, model giữa và downstreamKiểm tra một luồng liên quan quanh model trung tâm
path:models/stagingCác model trong thư mục stagingPhát triển/kiểm tra lớp chuẩn hóa
dbt ls --profiles-dir profiles --select stg_orders+ --resource-type model
dbt run --profiles-dir profiles --select stg_orders+

dbt ls --profiles-dir profiles --select +mart_daily_revenue --resource-type model
dbt run --profiles-dir profiles --select +mart_daily_revenue

Chọn downstream không tự bổ sung mọi upstream khác của những model đó. Khi chạy stg_orders+, int_orders_enriched còn cần int_order_amounts từ nhánh items; relation đó phải có sẵn và đúng phiên bản. Với môi trường mới trống, dùng selector có upstream hoặc build toàn project. Đây là lý do không thể xem dấu + như một công tắc “chạy mọi thứ cần thiết” trong mọi vị trí.

Nếu chọn hai tiêu chí cách nhau bằng khoảng trắng, thường đó là union: tài nguyên thuộc một trong các lựa chọn. Dấu phẩy biểu diễn intersection trong cú pháp selection. Khi selector phức tạp, luôn kiểm bằng ls và đặt selector trong dấu ngoặc kép để shell không diễn giải ký tự đặc biệt. Không học bằng cách thử ngay trên môi trường production.

dbt ls --profiles-dir profiles \
  --select "path:models/staging,stg_orders" --resource-type model

Ví dụ giao hai tập ở trên chỉ nên chọn stg_orders. dbt run chạy SQL models, còn dbt build có thể xử lý thêm tài nguyên như tests, seeds và snapshots theo lựa chọn và dependency. Chọn build không có nghĩa mọi bài toán chất lượng đã được kiểm: project phải khai báo test thích hợp trước.

5. Thêm nhánh báo cáo doanh thu theo khách hàng

Tạo models/marts/mart_customer_revenue.sql. Nhánh mới dùng lại int_orders_enriched cho tổng tiền đơn và stg_customers cho tên/thành phố. Không viết lại từ raw và copy logic trim/lower vào báo cáo thứ hai.

with paid_by_customer as (
    select
        customer_id,
        count(*) as paid_order_count,
        sum(order_amount_vnd) as paid_revenue_vnd
    from {{ ref('int_orders_enriched') }}
    where order_status = 'paid'
    group by customer_id
)
select
    c.customer_id,
    c.customer_name,
    c.city,
    coalesce(p.paid_order_count, 0) as paid_order_count,
    coalesce(p.paid_revenue_vnd, 0)::numeric(18,0) as paid_revenue_vnd
from {{ ref('stg_customers') }} as c
left join paid_by_customer as p
    on c.customer_id = p.customer_id

paid_by_customer đã có một dòng mỗi khách trước khi join. LEFT JOIN giữ cả Dung chưa có đơn. Coalesce thành 0 ở đây có ý nghĩa rõ: khách không có nhóm đơn paid trong báo cáo này. Nó không phải cách che tổng tiền null của một đơn paid thiếu item; trường hợp dữ liệu lỗi vẫn cần kiểm tra ở tầng đơn.

dbt run --profiles-dir profiles --select +mart_customer_revenue

docker compose exec -T postgres psql -U dbt_lab -d retail \
  -c "select * from dev_marts.mart_customer_revenue order by customer_id;"
KháchĐơn paidDoanh thu
An2350.000 VND
Binh150.000 VND
Chi00 VND
Dung00 VND

Tổng customer mart vẫn phải bằng 400.000 VND của daily mart. Tuy nhiên, tổng khớp không chứng minh mọi khách đúng: nếu order trỏ đến customer_id không có trong danh mục, cách LEFT JOIN bắt đầu từ customers sẽ không có dòng chứa đơn mồ côi đó. Cần test quan hệ orders.customer_id với customers.customer_id; bài 5 sẽ bổ sung các kiểm tra để không dựa riêng vào tính đầy đủ của dữ liệu mẫu.

Lineage giúp xem phạm vi cần kiểm tra khi một thành phần thay đổi.
Lineage giúp xem phạm vi cần kiểm tra khi một thành phần thay đổi.

6. Thêm mô tả để lineage có ý nghĩa

Tạo models/_models.yml. Tên file có thể khác, nhưng không khai báo lặp cùng model trong nhiều file properties. Đặt mô tả grain và định nghĩa chỉ tiêu trước những câu mô tả chung chung như “bảng doanh thu”.

version: 2
models:
  - name: int_orders_enriched
    description: 'Mot dong moi order_id, giu moi trang thai don'
    columns:
      - name: order_id
        description: 'Khoa don hang, khong phai khoa dong san pham'
      - name: order_amount_vnd
        description: 'Tong quantity * gia ban VND tai giao dich; null neu khong co item'
  - name: mart_daily_revenue
    description: 'Mot dong moi ngay co don paid; chua tinh thue, phi va hoan tien'
    columns:
      - name: paid_revenue_vnd
        description: 'Tong tien dong hang cua don paid theo ngay dat don'
  - name: mart_customer_revenue
    description: 'Mot dong moi khach trong danh muc; khong co don paid thi doanh thu 0'
dbt docs generate --profiles-dir profiles
dbt docs serve --profiles-dir profiles --port 8088 --host 127.0.0.1

Với Core 1.x của lab, mở địa chỉ local mà lệnh serve hiển thị, thường là http://127.0.0.1:8088. Dùng trang model và lineage graph để xem các nhánh gặp ở int_orders_enriched rồi chia sang hai mart. Nhấn Ctrl+C ở terminal để dừng server docs. Đây là server xem tài liệu local, không phải dịch vụ production được bảo mật sẵn.

docs generate đọc metadata cần thiết của project/database; tài liệu phải được tạo lại sau thay đổi để phản ánh trạng thái mới. Lineage cho biết dependency được khai báo, không tự chứng minh mọi câu join đúng hoặc mọi dashboard ngoài dbt đã được ghi nhận. Nếu muốn thể hiện dashboard phụ thuộc model, cần khai báo exposure/quy trình metadata phù hợp.

7. Thử lỗi có kiểm soát và đọc đúng tầng lỗi

Tạo file tạm models/intermediate/int_broken_demo.sql để xem lỗi ref sai tên. Không sửa model đang dùng của project.

select *
from {{ ref('stg_order_missing') }}
dbt parse --profiles-dir profiles

Kỳ vọng dbt báo model phụ thuộc node không tìm thấy. Đây là lỗi graph/khai báo trước khi SQL của model đó được thực thi. Sau thử nghiệm, chuyển file tạm ra ngoài model-paths để project parse lại được:

mkdir -p scratch_examples
mv models/intermediate/int_broken_demo.sql scratch_examples/
dbt parse --profiles-dir profiles

Tiếp theo, chỉ đọc file compiled của mart_customer_revenue bằng dbt compile. Nếu tên source đúng nhưng bảng vật lý không có, parse có thể vẫn qua trong khi run thất bại ở database. Nếu SQL sai cột, database trả lỗi cột. Phân biệt parse, compile và execute giúp bạn không sửa profile để chữa một lỗi SQL.

dbt compile --profiles-dir profiles --select mart_customer_revenue
cat target/compiled/retail_analytics/models/marts/mart_customer_revenue.sql

8. Tách môi trường bằng target schema

Một lợi ích của ref là các model chuyển theo target/schema mà không cần sửa tên bảng trong SQL. Để thử, thêm qa dưới outputs trong profiles/profiles.yml, ngang cấp với dev, giữ cùng host/database/credential nhưng schema: qa. File đầy đủ có thể viết như sau:

retail_analytics:
  target: dev
  outputs:
    dev: &base
      type: postgres
      host: 127.0.0.1
      port: 55432
      user: dbt_lab
      password: "{{ env_var('DBT_PASSWORD') }}"
      dbname: retail
      schema: dev
      threads: 2
      connect_timeout: 10
    qa:
      <<: *base
      schema: qa

YAML anchor &base cùng alias *base chỉ tái sử dụng cấu hình để tránh lặp. Cả hai target vẫn đọc cùng raw trong lab, nên đây là tách output schema, chưa phải isolation toàn bộ dữ liệu hoặc quyền. Trong doanh nghiệp, môi trường cần tách tài khoản, database hoặc dữ liệu nhạy cảm theo yêu cầu thực tế.

dbt run --profiles-dir profiles --target qa --select +mart_customer_revenue

docker compose exec -T postgres psql -U dbt_lab -d retail \
  -c "select * from qa_marts.mart_customer_revenue order by customer_id;"

Kết quả được xây vào qa_staging, qa_intermediate và qa_marts theo cơ chế custom schema mặc định. SQL source vẫn đọc raw vì _sources.yml chỉ định raw. Sau thử nghiệm, các lệnh không có --target quay về dev theo target: dev trong profile. Không đổi macro generate_schema_name chỉ để bỏ tiền tố khi chưa hiểu rủi ro ghi đè giữa các người dùng.

9. Bài tập nghiệm thu

Dùng ls để trả lời: thay đổi stg_customers ảnh hưởng mart nào? Trong project này, mart_customer_revenue cần cập nhật, còn mart_daily_revenue không phụ thuộc stg_customers. Thay đổi int_order_amounts thì cả hai mart chịu ảnh hưởng qua int_orders_enriched.

Tự chạy dbt ls với selector +mart_customer_revenue rồi đếm danh sách model. Bạn phải thấy cả nhánh stg_customers và nhánh orders/items, nhưng không cần stg_products vì báo cáo chưa phân tích theo sản phẩm. Sau đó, chạy hai mart và đối soát tổng 400.000 VND. Nếu khác, kiểm tra trạng thái đơn 1004 đã hoàn nguyên hay chưa trước khi nghi ngờ ref.

Bài hoàn thành khi bạn đọc được source logic khác schema vật lý, giải thích được ref vừa resolve relation vừa khai báo dependency, chọn đúng upstream/downstream và xây được output qa mà không sửa SQL model. Bài 5 sẽ bổ sung các chiều phân tích sản phẩm/khách/ngày và fact ở grain dòng hàng.

Tài liệu tham khảo

Scroll to Top