# dbt nam 2026: Chuyển đổi Dữ liệu, Kiểm thử và Câu hỏi Phỏng vấn > Hướng dẫn dbt cho kỹ sư dữ liệu: chuyển đổi SQL, mô hình phân lớp, chiến lược incremental, kiểm thử dữ liệu và câu hỏi phỏng vấn kỹ thuật với các ví dụ mã cho năm 2026. - Published: 2026-05-25 - Updated: 2026-05-25 - Author: SharpSkill - Tags: dbt, data-engineering, data-transformation, sql, interview - Reading time: 9 min --- dbt (data build tool) đã trở thành framework chuẩn để chuyển đổi dữ liệu bên trong các data warehouse hiện đại. Tính đến năm 2026, hơn 40.000 công ty sử dụng dbt trong môi trường production. Bài hướng dẫn này đề cập đến các cơ chế cốt lõi của việc chuyển đổi trong dbt, chiến lược kiểm thử, và những câu hỏi thường xuất hiện trong các buổi phỏng vấn data engineering. > **dbt làm gì** > > dbt xử lý chữ T trong ELT. Nó biên dịch các câu lệnh SQL SELECT thành DDL (CREATE TABLE, CREATE VIEW, MERGE) và thực thi chúng trên warehouse — Snowflake, BigQuery, Redshift, hoặc Databricks. Quản lý phiên bản, phân giải phụ thuộc, kiểm thử và tài liệu được tích hợp sẵn. ## Mô hình Phân lớp: Staging, Intermediate và Marts Một dự án dbt được cấu trúc tốt sẽ phân tách các phep chuyển đổi thành ba lớp. Cách tiếp cận này, được cộng đồng dbt phổ biến, áp dụng nguyên tắc single-responsibility cho mỗi model và giúp việc gỡ lỗi trở nên đơn giản. **Staging** là lớp gần nhất với dữ liệu thô. Nhiệm vụ của lớp này rất hẹp: đổi tên cột, ép kiểu dữ liệu, và lọc các hàng không hợp lệ. Không join, không aggregation. ```sql -- models/staging/stg_orders.sql WITH source AS ( SELECT * FROM {{ source('ecommerce', 'raw_orders') }} ) SELECT id AS order_id, customer_id, CAST(order_date AS DATE) AS order_date, CAST(amount AS DECIMAL(10, 2)) AS order_amount, LOWER(status) AS order_status FROM source WHERE id IS NOT NULL ``` **Intermediate** áp dụng logic nghiệp vụ — join, aggregation, window function. Các model này tham chiếu đến staging model thông qua `ref()`, hàm này đăng ký các phụ thuộc trong DAG. ```sql -- models/intermediate/int_customer_orders.sql WITH orders AS ( SELECT * FROM {{ ref('stg_orders') }} ), customers AS ( SELECT * FROM {{ ref('stg_customers') }} ) SELECT c.customer_id, c.customer_name, COUNT(o.order_id) AS total_orders, SUM(o.order_amount) AS lifetime_value, MIN(o.order_date) AS first_order_date, MAX(o.order_date) AS last_order_date FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.customer_name ``` **Marts** là lớp tiêu thụ cuối cùng — các bảng sạch, có tài liệu, mà dashboard và analyst truy vấn trực tiếp. Cách tiếp cận phân lớp này đảm bảo rằng một nguồn dữ liệu bị hỏng chỉ ảnh hưởng đến staging, không phải toàn bộ pipeline. Mỗi lớp có thể được kiểm thử độc lập. ## Hàm ref() và Phân giải DAG `ref()` không phải là một alias đơn giản. Khi gọi `{{ ref('stg_orders') }}`, hai việc xảy ra: nó phân giải tên bảng đầy đủ tại thời điểm biên dịch, và nó đăng ký một cạnh phụ thuộc trong directed acyclic graph (DAG) của dbt. Không có `ref()`, dbt không có cách nào xác định thứ tự thực thi. Một lỗi phổ biến là viết trực tiếp tên bảng thay vì sử dụng `ref()`. Điều này phá vỡ việc theo dõi phụ thuộc và có thể dẫn đến việc các model chạy trước khi các phụ thuộc upstream sẵn sàng. ```sql -- Sai: tham chiếu hardcode, không có theo dõi phụ thuộc SELECT * FROM analytics.stg_orders -- Đúng: ref() đăng ký phụ thuộc SELECT * FROM {{ ref('stg_orders') }} ``` DAG cũng hỗ trợ các tính năng như `dbt run --select stg_orders+`, chạy một model và tất cả model phía downstream — hữu ích cho việc rebuild có mục tiêu sau khi thay đổi schema nguồn. ## Materialization: Chọn Chiến lược Lưu trữ Phù hợp dbt cung cấp bốn materialization tích hợp sẵn, mỗi loại phù hợp với các mẫu truy cập và khối lượng dữ liệu khác nhau. | Materialization | Lưu trữ | Phù hợp cho | Đánh đổi | |----------------|---------|----------|----------| | `view` | Không lưu (tính khi đọc) | Biến đổi nhẹ, dataset nhỏ | Đọc chậm với dữ liệu lớn | | `table` | Tái tạo bảng hoàn toàn mỗi lần chạy | Model lớp mart, đọc nhanh | Tái tạo toàn bộ, chi phí cao | | `incremental` | Chỉ thêm/merge hàng mới | Bảng fact lớn, event stream | Logic phức tạp hơn, cần `unique_key` | | `ephemeral` | Inline như CTE, không bao giờ được lưu | Logic tái sử dụng giữa các model | Không thể truy vấn trực tiếp | Mặc định là `view`. Ghi đè trong khối config của model hoặc trong `dbt_project.yml`: ```sql -- models/marts/fct_daily_revenue.sql {{ config( materialized='incremental', unique_key='revenue_date', incremental_strategy='merge' ) }} SELECT DATE(order_date) AS revenue_date, SUM(order_amount) AS daily_revenue, COUNT(DISTINCT customer_id) AS unique_customers FROM {{ ref('stg_orders') }} WHERE order_status = 'completed' {% if is_incremental() %} AND order_date > (SELECT MAX(revenue_date) FROM {{ this }}) {% endif %} GROUP BY DATE(order_date) ``` Khối `is_incremental()` chỉ chạy trong quá trình build incremental — khi full refresh (`dbt run --full-refresh`), khối này bị bỏ qua và toàn bộ bảng được xây dựng lại. ## Kiểm thử Chất lượng Dữ liệu Ở Mỗi Lớp dbt cung cấp hai loại kiểm thử: **generic tests** khai báo trong YAML, và **singular tests** viết dưới dạng file SQL độc lập trả về các hàng khi thất bại. Generic tests bao gồm các ràng buộc cấu trúc: ```yaml # models/staging/_stg_models.yml version: 2 models: - name: stg_orders columns: - name: order_id tests: - unique # Không có order ID trùng lặp - not_null # Mọi hàng đều có order ID - name: order_status tests: - accepted_values: values: ['completed', 'pending', 'cancelled', 'refunded'] - name: customer_id tests: - not_null - relationships: # Kiểm tra tính toàn vẹn tham chiếu to: ref('stg_customers') field: customer_id ``` Singular tests xác thực các quy tắc nghiệp vụ mà generic tests không thể biểu đạt: ```sql -- tests/assert_revenue_never_negative.sql -- Trả về các hàng có doanh thu hàng ngày âm (kỳ vọng 0 hàng) SELECT revenue_date, daily_revenue FROM {{ ref('fct_daily_revenue') }} WHERE daily_revenue < 0 ``` **Unit tests**, được giới thiệu trong dbt 1.8, xác thực logic chuyển đổi với đầu vào được kiểm soát và đầu ra mong đợi — không cần warehouse. Chúng chạy trực tiếp trong giai đoạn dbt test. Chiến lược kiểm thử nên theo dòng chảy dữ liệu: kiểm tra freshness ở mức source, xác thực ràng buộc schema ở staging, toàn vẹn tham chiếu ở intermediate, và xác nhận logic nghiệp vụ ở mart. ## dbt Core v1.10 và Fusion Engine dbt Core v1.10 giới thiệu cờ `--sample` cho các lệnh `run` và `build`. Cờ này áp dụng lấy mẫu dựa trên thời gian cho `ref()` và `source()`, cho phép developer xác thực các phép chuyển đổi trên một tập con dữ liệu mà không tốn chi phí full build. Rất hữu ích khi làm việc với các model được hỗ trợ bởi bảng fact chứa hàng tỷ hàng. Fusion engine — viết lại hoàn toàn bằng Rust — hiện là mặc định cho các dự án mới trên dbt Cloud với Snowflake, BigQuery, Redshift và Databricks. Fusion giới thiệu semantic versioning bắt đầu từ 2.0 và mang lại cải thiện hiệu suất đáng kể cho quá trình biên dịch và thực thi. Các bổ sung đáng chú ý khác trong 2026: đặc tả YAML cho Semantic Layer để định nghĩa metric tập trung, Cost Insights (beta) để ước tính chi phí tính toán warehouse mỗi model, và native private packages hiện đã khả dụng rộng rãi. ## Macro và Jinja cho Logic Tái sử dụng Khi cùng một mẫu SQL xuất hiện trong nhiều model, hãy trích xuất nó thành macro. Macro là các hàm Jinja được lưu trong thư mục `macros/`. ```sql -- macros/cents_to_dollars.sql {% macro cents_to_dollars(column_name) %} ROUND(CAST({{ column_name }} AS DECIMAL(10, 4)) / 100, 2) {% endmacro %} ``` Gọi trong bất kỳ model nào: ```sql -- models/staging/stg_payments.sql SELECT payment_id, order_id, {{ cents_to_dollars('amount_cents') }} AS amount_dollars, payment_method FROM {{ source('stripe', 'payments') }} ``` Cách này giữ cho codebase tuân thủ nguyên tắc DRY. Phương án thay thế — sao chép công thức chuyển đổi vào mọi model cần nó — tạo ra gánh nặng bảo trì và sự không nhất quán. ## Câu hỏi Phỏng vấn Thường Gặp Các buổi phỏng vấn data engineering năm 2026 ngày càng kiểm tra kiến thức dbt vượt xa những điều cơ bản. Dưới đây là những câu hỏi phân biệt các ứng viên, được rút ra từ các mẫu thường thấy tại các công ty sử dụng modern data stack. **H: Một model incremental đã xử lý lại toàn bộ bảng qua đêm. Chuyện gì đã xảy ra?** Nguyên nhân phổ biến nhất: cột `unique_key` chứa giá trị null. Khi dbt cố gắng MERGE trên key null, việc khớp thất bại cho mọi hàng, dẫn đến insert trùng lặp. Nguyên nhân thứ hai: ai đó chạy `dbt run --full-refresh` mà không nhận ra rằng nó xây dựng lại bảng từ đầu. Thứ ba: bộ lọc `is_incremental()` tham chiếu một cột chưa tồn tại trong bảng đích (lần chạy đầu tiên so với các lần chạy tiếp theo). Việc gỡ lỗi bắt đầu bằng việc kiểm tra SQL đã biên dịch trong `target/compiled/`. **H: Kiểm thử nên được phân lớp như thế nào trong dự án dbt production?** Kiểm tra freshness của source chạy trước — nếu source chưa được cập nhật, các model downstream không nên chạy trên dữ liệu cũ. Kiểm thử staging xác thực ràng buộc schema: primary key (`unique` + `not_null`), giá trị chấp nhận, và ép kiểu. Kiểm thử intermediate kiểm tra toàn vẹn tham chiếu giữa các join. Kiểm thử mart xác nhận các bất biến nghiệp vụ: revenue >= 0, active users >= paying users, và tổng các phần bằng tổng thể. Tất cả test chạy trong CI trên pull request đối với schema dev, sau đó chạy lại sau khi triển khai trong production với cảnh báo. **H: Khi nào một model nên là ephemeral so với view?** Model ephemeral inline SQL của nó như một CTE vào các model tiêu thụ — hữu ích cho logic tái sử dụng không cần truy vấn độc lập. View tính toán khi đọc và tồn tại như đối tượng có thể truy vấn. Đánh đổi: model ephemeral không thể kiểm thử trực tiếp (không có bảng để chạy assertion) và không xuất hiện trong các data lineage tool. Sử dụng ephemeral cho logic helper nội bộ; sử dụng view cho bất cứ thứ gì cần kiểm thử hoặc giám sát độc lập. **H: Giải thích sự khác biệt giữa `source()` và `ref()`.** `source()` trỏ đến các bảng thô được định nghĩa trong file `sources.yml` — đây là các bảng mà dbt không quản lý. `ref()` trỏ đến các model dbt khác. Cả hai đều đăng ký phụ thuộc DAG, nhưng `source()` còn cho phép kiểm tra freshness (`dbt source freshness`), điều mà `ref()` không làm. Sử dụng `source()` cho bảng thô và `ref()` cho mọi thứ khác không phải là tùy chọn — đó là cách dbt biết những gì nó kiểm soát và những gì nó không kiểm soát. Để có bộ câu hỏi phỏng vấn data engineering rộng hơn, bao gồm các chủ đề như kiến trúc pipeline ETL và ELT, các module luyện tập SharpSkill về dbt fundamentals và advanced dbt patterns đề cập các khái niệm này với các bài tập tương tác. ## Kết luận - dbt chuyển đổi dữ liệu thô trong warehouse thông qua các câu lệnh SQL SELECT, được biên dịch tự động thành DDL — cấu trúc phân lớp staging/intermediate/mart giữ cho mỗi model tập trung vào một trách nhiệm duy nhất - Hàm `ref()` là xương sống của quản lý phụ thuộc: phân giải tên bảng và xây dựng DAG điều khiển thứ tự thực thi - Materialization incremental giảm chi phí cho bảng lớn, nhưng đòi hỏi xử lý cẩn thận `unique_key`, giá trị null, và bộ lọc `is_incremental()` - Kiểm thử nên theo dòng chảy dữ liệu — freshness source, ràng buộc schema ở staging, toàn vẹn tham chiếu ở intermediate, xác nhận logic nghiệp vụ ở mức mart - dbt Core v1.10 mang đến cờ `--sample` cho việc lặp nhanh hơn, trong khi Fusion engine (dựa trên Rust, phiên bản 2.0) hiện là mặc định trên dbt Cloud - Câu hỏi phỏng vấn về dbt tập trung vào việc gỡ lỗi các thất bại thực tế (xử lý lại incremental, null key, source cũ) chứ không phải định nghĩa — kinh nghiệm thực tế với SQL đã biên dịch và CI pipeline quan trọng hơn câu trả lời thuộc lòng --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/data-engineering/dbt-data-transformations-testing-interview-2026