1 Tổng quan

Bài học này nhằm hệ thống hoá các câu lệnh SQL cần thiết cho data engineering. Đây không phải là một bài học dạy cơ bản cách sử dụng từng câu lệnh một mà sẽ chỉ đưa ra thêm thông tin và ví dụ với những câu lệnh mà mình cho là không quá dễ hiểu đối với người mới bắt đầu với lĩnh vực data engineering hoặc có những điểm lưu ý quan trọng mà người mới cần nắm được.

2 Các nhóm câu lệnh SQL chính

2.1 Nhóm câu lệnh DDL (data definition language)

Đây là nhóm các câu lệnh dùng để định nghĩa và thay đổi các cấu trúc trong cơ sở dữ liệu.

Nhóm này gồm các câu lệnh CREATE, DROP, ALTER, TRUNCATE dùng để tạo mới, thay đổi, xoá schema, bảng, views, index, partition, constraint.

2.2 Nhóm câu lệnh DML (data manipulation language)

Đây là nhóm các câu lệnh dùng để tạo mới dữ liệu, biến đổi và xoá dữ liệu.

Nhóm này gồm các câu lệnh như INSERT, UPDATE, DELETE, MERGE, UPSERT, …

Ta sẽ cùng xem một ví dụ về MERGEUPSERT (các câu lệnh khác các bạn có thể tự đọc và tìm hiểu

2.3 Nhóm câu lệnh DQL (data query language)

Đây là nhóm câu lệnh dùng để lấy dữ liệu ra khỏi database và thưc hiện một số biến đổi dữ liệu.

Nhóm này gồm các câu lệnh:

  • SELECT

  • DISTINCT

  • FROM

  • WHERE

  • GROUP BY

  • HAVING

  • ORDER BY

  • LIMIT

Khi viết lệnh để query dữ liệu từ các bảng trong database, ta cần lưu ý Order of execution/operation trong SQL. Xem thêm tại bài học “Lesson 2” ở khoá học Tổng ôn SQL cho người mới bắt đầu.

2.4 Nhóm câu lệnh TCL (Transaction Control Language)

Đây là nhóm câu lệnh để thực hiện và quản lý các giao dịch (transaction).

Một transaction (giao dịch) bao gồm một hoặc nhiều tác vụ. Mỗi transaction bắt đầu bằng một tác vụ và kết thúc khi tất cả các tác vụ đều được thực hiện thành công. Nếu có bất kỳ tác vụ nào thất bại thì cả transaction sẽ thất bại (fail). Vì vậy, một transaction chỉ có thể có một trong hai kết quả là thành công hoặc thất bại (chứ không có thành công một phần). Việc sử dụng transaction giúp đảm bảo ETL pipeline không lưu dữ liệu không hoàn chỉnh vào database.

Nhóm này bao gồm các câu lệnh như BEGIN TRANSACTION, COMMIT, ROLLBACK, SAVEPOINT, …

Ví dụ: Giả sử ta có bảng dữ liệu giao dịch thanh toán hoá đơn của khách hàng như sau trong SQL Server database:

DROP TABLE IF EXISTS mydb.dbo.payment;
DROP TABLE IF EXISTS mydb.dbo.etl_log;

-- Create payment table
CREATE TABLE mydb.dbo.payment(
    payment_id INT PRIMARY KEY,
    customer_id INT,
    message VARCHAR(50)
)

-- Insert data to table
INSERT INTO mydb.dbo.payment
(payment_id, customer_id, message)
VALUES
    (1, 11, 'thanh toan hop dong'),
    (2, 11, 'thanh toan hop dong'),
    (3, 22, 'thanh toan hop dong'),
    (4, 33, 'thanh toan hop dong'),
    (5, 9999, 'testing')
;

SELECT * FROM mydb.dbo.payment;
payment_id customer_id message
1 11 thanh toan hop dong
2 11 thanh toan hop dong
3 22 thanh toan hop dong
4 33 thanh toan hop dong
5 9999 testing

Giả sử ta cần hot fix cập nhật dữ liệu ở bảng này, do ta phát hiện có lỗi dữ liệu. Ta cần xoá giáo dịch có payment_id = 5 và cần cập nhật lại cột message của giao dịch có payment_id = 4

DELETE FROM mydb.dbo.payment WHERE payment_id = 5

UPDATE mydb.dbo.payment
SET message = 'thanh toan hop dong XYZ12345 cho khach hang VIP voi chiet khau 20 phan tram'
WHERE payment_id = 4

Tuy nhiên, ta sẽ thấy một vấn đề nghiêm trọng ở đây, đó là câu lệnh DELETE FROM đã được chạy thành công, xoá đi bản ghi có payment_id = 5, nhưng câu lệnh UPDATE thì lại báo lỗi sau:

String or binary data would be truncated in table 'mydb.dbo.payment', column 'message'. Truncated value: 'thanh toan hop dong XYZ12345 cho khach hang VIP vo'.
The statement has been terminated.

Kiểm tra dữ liệu ở bảng payment ta thấy message của payment_id = 4 đúng là vẫn chưa được update.

payment_id customer_id message
1 11 thanh toan hop dong
2 11 thanh toan hop dong
3 22 thanh toan hop dong
4 33 thanh toan hop dong

Lí do là vì cột message có kiểu dữ liệu VARCHAR(50) và chỉ chứa được 50 ký tự, trong khi message ta muốn cập nhật vào lại quá dài.

Như vậy ta rơi vào tình huống bảng bị cập nhật nửa vời và sẽ có thể phát sinh nhiều vấn đề.

Để tránh tình trạng này xảy ra, ta sẽ cho tất cả các ETL logic vào trong một “transaction” của SQL Server database. Ta sử dụng lệnh BEGIN TRANSACTION để bắt đầu transaction và COMMIT TRANSACTION hoặc ROLLBACK TRANSACTION để kết thúc transaction. Ta dùng thêm các lệnh try (BEGIN TRY...END TRY), catch (BEGIN CATCH...END CATCH) để xử lý exception. Khi logic trong (BEGIN TRY...END TRY) chạy bình thường thì ta sẽ COMMIT TRANSACTION , còn khi có lỗi (exception) xảy ra thì logic trong (BEGIN CATCH...END CATCH) sẽ được thực hiện. Tại đây ta thực hiện ROLLBACK TRANSACTION.

DROP TABLE IF EXISTS mydb.dbo.etl_log

CREATE TABLE mydb.dbo.etl_log(
    log_id INT PRIMARY KEY IDENTITY,
    job_name VARCHAR(50),
    start_time DATETIME,
    end_time DATETIME,
    status VARCHAR(10),
    error_message VARCHAR(MAX)
)

DECLARE @log_id INT;

-- log etl job start
INSERT INTO mydb.dbo.etl_log
(job_name, start_time, end_time, status, error_message)
VALUES('hot fix payment table', GETDATE(), NULL, 'RUNNING', NULL)

BEGIN TRANSACTION
BEGIN TRY    
    -- capture inserted identity
    SET @log_id = SCOPE_IDENTITY();
    SELECT @log_id

    -- update payment table
    DELETE FROM mydb.dbo.payment WHERE payment_id = 5

    UPDATE mydb.dbo.payment
    SET message = 'thanh toan hop dong XYZ12345 cho khach hang VIP voi chiet khau 20 phan tram'
    WHERE payment_id = 4

    -- log etl job end
    UPDATE mydb.dbo.etl_log
    SET end_time = GETDATE(), status = 'SUCCESS'
    WHERE log_id = @log_id

    COMMIT TRANSACTION
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION

    -- log etl job end
    UPDATE mydb.dbo.etl_log
    SET end_time = GETDATE(), status = 'FAILED', error_message = ERROR_MESSAGE()
    WHERE log_id = @log_id
END CATCH

Kiểm tra bảng payment ta thấy dữ liệu không có gì thay đổi (payment_id = 5 vẫn tồn tại và payment_id = 4 vẫn chưa bị update). Kiểm tra bảng etl_log ta thấy như sau:

log_id job_name start_time end_time status error_message
1 hot fix payment table 2025-09-19 04:25:54.623 2025-09-19 04:25:54.633 FAILED String or binary data would be truncated in table ‘mydb.dbo.payment’, column ‘message’. Truncated value: ‘thanh toan hop dong XYZ12345 cho khach hang VIP vo’.

Vậy là lỗi phát sinh khi chạy lệnh UPDATE đã khiến cho transaction thất bại và SQL Server roll back lại các câu lệnh đã thực hiện và dữ liệu của bảng payment trở về như cũ, ta không còn bị rơi vào tình trạng một vài câu lệnh đã được thực hiện còn các câu lệnh khác thì chưa được thực hiện nữa.

2.5 Nhóm câu lệnh DCL (Data Control Language)

Đây là nhóm câu lệnh liên quan tới quản trị và bảo mật. VD: tạo, cấp và phân quyền truy cập, che dấu (masking) dữ liệu, …

Nhóm này bao gồm các câu lệnh như GRANT, REVOKE, …

3 JOIN và WINDOW function

Việc nắm vững cách sử dụng JOIN và WINDOW function sẽ giúp Data Engineer xử lý được các logic biến đổi dữ liệu phức tạp.

Xem bài học “Lesson 02” ở khoá học Tổng ôn SQL cho người mới bắt đầu để nắm được một số lưu ý quan trọng khi sử dụng JOIN và WINDOW function.

4 CTEs (common table expression) và bảng tạm (temporary table)

CTE giúp ta viết code dễ hơn và rõ ràng hơn, đặc biệt là trong những trường hợp phải dùng nhiều subquery để xử lý các logic biến đổi dữ liệu phức tạp.

Bảng tạm cho phép ta lưu trữ dữ liệu tạm thời để có thể sử dụng nhiều lần trong 1 phiên làm việc (session), và cũng giúp ta xử lý các logic biến đổi dữ liệu phức tạp một cách dễ dàng hơn.

Xem bài học “Lesson 01 extra” ở khoá học Tổng ôn SQL cho người mới bắt đầu để ôn tập (mức độ cơ bản) về 2 khái niệm này.

5 User-defined function và Stored procedure

5.1 User-defined function

Trong các hệ thống cơ sở dữ liệu quan hệ (vd: MySQL, SQL Server, …) ta có thể chia các function (hàm) thành 2 loại như sau:

  • Built-in function: Các hàm có sẵn trong hệ thống

  • User-defined function: Các hàm được tạo ra bởi người dùng

Ví dụ với SQL Server, ta có các built-in function thường dùng như CAST, COALESCE, DATEADD, DATEDIFF, COUNT, SUM, SUBSTRING, LOWER, …

Sau đây ta sẽ tập trung nói về việc tạo các function mà chưa có sẵn trong hệ thống (các user-defined function) để phục vụ cho công việc data engineering. Ta sẽ tập trung vào các loại function mà ta dùng lệnh SQL để tạo ra (chúng sẽ được lưu dưới dạng các object trong database), và sẽ không nói đến các function được tạo ra bằng cách viết code sử dụng các ngôn ngữ lập trình khác (VD: “loadable function” trong MySQL phải được viết bằng C++).

Với user-defined function, ta có thể chia thành các loại chính sau:

  • Scalar function: Hàm trả ra một giá trị duy nhất, thường được dùng để tính toán hoặc để format dữ liệu.

  • Table-returning function/Set-returning function: Hàm trả ra kết quả dưới dạng bảng (tức là có thể gồm nhiều dòng, cột), thường sẽ có performance tốt hơn scalar function vì hoạt động tốt với optimiser của database.

Ví dụ trong SQL Server:

Ta có bảng dữ liệu test_customers như sau

USE mydb

DROP TABLE IF EXISTS mydb.dbo.test_customers

CREATE TABLE mydb.dbo.test_customers(
    id INT,
    name VARCHAR(100),
    type VARCHAR(50),
    gender VARCHAR(20)
)

INSERT INTO mydb.dbo.test_customers
(id, name, type, gender)
VALUES
    (1, 'Tuan', 'VIP', 'Male'),
    (2, 'Thanh', 'Normal', 'Male'),
    (3, 'Trang', 'VIP', 'Female'),
    (4, 'Tung', 'Normal', 'Other'),
    (5, 'Linh', 'VIP', 'Female')

Ta tạo một số function như sau:

USE mydb
GO

DROP FUNCTION IF EXISTS dbo.fn_GetCustomerByTypeAndGender
DROP FUNCTION IF EXISTS dbo.StandardizeGender
GO

-- Scalar function
CREATE FUNCTION dbo.StandardizeGender
(
    @Gender VARCHAR(20)
)
RETURNS VARCHAR(1)
AS
BEGIN
/****************************************************************************************************
Description:     
    A scalar user-defined function that standardizes various textual representations of gender 
    into a single-character code. It is designed to ensure consistent gender encoding across 
    source systems and downstream analytical layers.

Input Parameters:
    @Gender  VARCHAR(20)
        The gender value as provided by the source system (e.g., 'Male', 'Female', 'M', 'F', 'Unknown').

Return Type:
    VARCHAR(1)
        Returns one of the following standardized gender codes:
            'M' – Male
            'F' – Female
            'O' – Other or Unknown
****************************************************************************************************/
    RETURN 
        CASE 
            WHEN @Gender = 'Male' THEN 'M'
            WHEN @Gender = 'Female' THEN 'F'
            ELSE 'O'
        END
END
GO

-- table-returning function (the name in SQL Server is table-valued function)
CREATE FUNCTION dbo.fn_GetCustomerByTypeAndGender
(
    @CustType VARCHAR(50),
    @Gender VARCHAR(20)
)
RETURNS @FilteredCustomers TABLE
(
    id INT,
    name VARCHAR(100),
    type VARCHAR(50),
    gender VARCHAR(20)
)
AS
BEGIN
/****************************************************************************************************
Description:     
    A table-valued user-defined function (TVF) that filters and returns customers 
    from the dbo.test_customers table based on customer type and gender. 

Input Parameters:
    @CustType   VARCHAR(50)
        The customer type to filter on (e.g., 'VIP', 'Normal', 'VVIP', ...).

    @Gender     VARCHAR(20)
        The gender of the customer to filter on (e.g., 'Male', 'Female', 'Other').

Return Type:
    TABLE
        Returns a table containing customers that match both input parameters.
        The returned table schema includes:
            - id       INT          : Unique customer identifier
            - name     VARCHAR(100) : Customer name
            - type     VARCHAR(50)  : Customer type
            - gender   VARCHAR(20)  : Customer gender

Usage Example:
    SELECT * 
    FROM dbo.fn_GetCustomerByTypeAndGender('VIP', 'Female');
    
Notes:
    Ensure indexes exist on dbo.test_customers.type and dbo.test_customers.gender for optimal performance.
****************************************************************************************************/
    INSERT INTO @FilteredCustomers 
    (id, name, type, gender)
    SELECT
        id,
        name,
        type,
        gender
    FROM dbo.test_customers
    WHERE type = @CustType 
        AND gender = @Gender

    RETURN
END
GO

Ta sử dụng các function vừa tạo như sau:

SELECT
    id,
    name,
    type,
    dbo.StandardizeGender(gender) AS gender
FROM dbo.fn_GetCustomerByTypeAndGender('VIP', 'Female')
id name type gender
3 Trang VIP FALSE
5 Linh VIP FALSE

5.2 Stored procedure

Stored procedure là một hoặc một chuỗi các câu lệnh SQL được lưu trữ lại trong database để ta có thể tái sử dụng nhiều lần. Chính vì vậy, stored procedure giúp ích rất nhiều cho các data engineer khi cần triển khai các logic biến đổi dữ liệu và tự động hoá các logic trong ETL pipeline.

Ngoài lợi ích về tái sử dụng code thì stored procedure còn có các lợi ích khác như giúp ta dễ maintain code hơn, giảm tải network traffic giữa server và client (vì tất cả code được thực hiện trong cùng 1 batch thay vì client phải gửi code nhiều lần sang server), … Bạn có thể đọc thêm tại đây: https://learn.microsoft.com/en-us/sql/relational-databases/stored-procedures/stored-procedures-database-engine?view=sql-server-ver17

Cũng tương tự như với function, ta cũng có những built-in stored procedure và user-defined stored procedure.

Stored procedure có thể có:

  • các input parameter (tham số đầu vào) để ta truyền vào cho stored procedure một số giá trị

  • các output parameter để chứa các giá trị mà ta muốn stored procedure trả ra

Ví dụ:

Ta có một ETL pipeline để load dữ liệu hàng ngày cho 1 bảng dim customer và một bảng fact order. Ta load dữ liệu từ một hệ thống nguồn vào các bảng staging (có tiền tố stg trong tên bảng), sau đó load dữ liệu từ bảng staging vào bảng dim/fact tương ứng.

Với bảng dim customer, ta thực hiện full load (mỗi lần load là xoá toàn bộ dữ liệu ở bảng đi và load mới lại).

Với bảng fact order, ta thực hiện incremental load (mỗi lần load chỉ load thêm dữ liệu mới vào bảng, không động đến dữ liệu cũ).

Ta tạo các bảng như sau:

USE mydb

DROP TABLE IF EXISTS mydb.dbo.stg_customers
DROP TABLE IF EXISTS mydb.dbo.d_customers
DROP TABLE IF EXISTS mydb.dbo.stg_orders
DROP TABLE IF EXISTS mydb.dbo.f_orders
DROP TABLE IF EXISTS mydb.dbo.etl_run_log

CREATE TABLE mydb.dbo.stg_orders(
    source_order_id INT,
    source_customer_id INT,
    order_amt FLOAT,
    order_datetime DATETIME,
    insert_timestamp DATETIME
)

CREATE TABLE mydb.dbo.f_orders(
    source_order_id INT PRIMARY KEY,
    customer_id INT,
    order_amt FLOAT,
    order_datetime DATETIME,
    insert_timestamp DATETIME
)

CREATE TABLE mydb.dbo.stg_customers(
    source_customer_id INT,
    name VARCHAR(100),
    type VARCHAR(50),
    gender VARCHAR(20),
    insert_timestamp DATETIME
)

CREATE TABLE mydb.dbo.d_customers(
    customer_id INT IDENTITY(1,1) PRIMARY KEY,
    source_customer_id INT UNIQUE,
    name VARCHAR(100),
    type VARCHAR(50),
    gender VARCHAR(20),
    insert_timestamp DATETIME
)

INSERT INTO mydb.dbo.stg_orders
(source_order_id, source_customer_id, order_amt, order_datetime, insert_timestamp)
VALUES
    (11, 333, 2500, '2025-01-18 08:02:00', GETDATE()),
    (22, 333, 10000, '2025-01-18 13:25:00', GETDATE()),
    (33, 555, 3000, '2025-01-18 14:08:00', GETDATE()),
    (44, 111, 1500, '2025-01-19 10:11:00', GETDATE())

INSERT INTO mydb.dbo.stg_customers
(source_customer_id, name, type, gender, insert_timestamp)
VALUES
    (111, 'Tuan', 'VIP', 'Male', GETDATE()),
    (222, 'Thanh', 'Normal', 'Male', GETDATE()),
    (333, 'Trang', 'VIP', 'Female', GETDATE()),
    (444, 'Tung', 'Normal', 'Other', GETDATE()),
    (555, 'Linh', 'VIP', 'Female', GETDATE())

CREATE TABLE mydb.dbo.etl_run_log(
    log_id INT IDENTITY(1,1) PRIMARY KEY,
    job_name VARCHAR(200),
    status VARCHAR(10),
    finished_datetime DATETIME,
    error_message VARCHAR(MAX)
)

Kiểm tra dữ liệu bảng stg_customers:

source_customer_id name type gender insert_timestamp
111 Tuan VIP Male 2025-09-21 06:48:13.413
222 Thanh Normal Male 2025-09-21 06:48:13.413
333 Trang VIP Female 2025-09-21 06:48:13.413
444 Tung Normal Other 2025-09-21 06:48:13.413
555 Linh VIP Female 2025-09-21 06:48:13.413

Kiểm tra dữ liệu bảng stg_orders:

source_order_id source_customer_id order_amt order_datetime insert_timestamp
11 333 2500 2025-01-18 08:02:00.000 2025-09-21 06:48:13.410
22 333 10000 2025-01-18 13:25:00.000 2025-09-21 06:48:13.410
33 555 3000 2025-01-18 14:08:00.000 2025-09-21 06:48:13.410
44 111 1500 2025-01-19 10:11:00.000 2025-09-21 06:48:13.410

Ta tạo các stored procedure như sau:

  • LogETL: để lữu dữ liệu vào bảng etl_run_log giúp ta theo dõi các pipeline chạy thành công hay thất bại và lỗi phát sinh là gì

  • LoadDimCustomers: chứa các logic để load dữ liệu từ bảng stg_customers vào bảng d_customers

  • LoadFactOrders: chứa các logic để load dữ liệu từ bảng stg_orders vào bảng f_orders

  • RunDailyETL: stored procedure này dùng để điều phối thứ tự chạy của các pipeline thông qua việc gọi các stored procedure khác. Theo thông lệ thì ta nên load dữ liệu cho các bảng dim trước rồi mới load dữ liệu cho bảng fact. Thứ tự chạy: dim customers -> fact orders.

DROP PROCEDURE IF EXISTS dbo.LogETL
DROP PROCEDURE IF EXISTS dbo.LoadDimCustomers
DROP PROCEDURE IF EXISTS dbo.LoadFactOrders
DROP PROCEDURE IF EXISTS dbo.RunDailyETL
GO

CREATE PROCEDURE dbo.LogETL
    @JobName VARCHAR(200),
    @Status VARCHAR(10),
    @ErrorMessage VARCHAR(MAX) = NULL
AS
    SET NOCOUNT ON

    INSERT INTO dbo.etl_run_log
    (job_name, status, finished_datetime, error_message)
    VALUES
    (@JobName, @Status, GETDATE(), @ErrorMessage)
GO

CREATE PROCEDURE dbo.LoadDimCustomers
AS
BEGIN
    SET NOCOUNT ON

    DECLARE @ETLJobName VARCHAR(100) = 'Load Customers Dimension Table'
    DECLARE @ErrorMessage VARCHAR(MAX)

    BEGIN TRANSACTION
    BEGIN TRY
        -- truncate table dim customer as we are using full load
        TRUNCATE TABLE mydb.dbo.d_customers

        -- insert data into dim customer
        INSERT INTO mydb.dbo.d_customers
        (source_customer_id, name, type, gender, insert_timestamp)
        SELECT 
            source_customer_id, 
            name, 
            type, 
            gender, 
            GETDATE() AS insert_timestamp
        FROM mydb.dbo.stg_customers

        -- log ETL success
        EXEC dbo.LogETL @ETLJobName, 'Success'

        -- commit transaction
        COMMIT TRANSACTION
    END TRY
    
    BEGIN CATCH
        ROLLBACK TRANSACTION
        -- log ETL failure
        SET @ErrorMessage = ERROR_MESSAGE()
        EXEC dbo.LogETL @ETLJobName, 'Failed', @ErrorMessage
    END CATCH
END
GO

CREATE PROCEDURE dbo.LoadFactOrders
AS
BEGIN
    SET NOCOUNT ON

    DECLARE @ETLJobName VARCHAR(100) = 'Load Orders Fact Table'
    DECLARE @ErrorMessage VARCHAR(MAX)

    BEGIN TRANSACTION
    BEGIN TRY

        -- insert data into orders fact table using incremental load strategy        
        WITH new_stg_orders AS(
            SELECT 
                source_order_id,
                source_customer_id,
                order_amt,
                order_datetime
            FROM mydb.dbo.stg_orders AS stg_orders
            -- logic to get all new records compared to the last time the fact orders table was updated
            WHERE order_datetime >= (SELECT COALESCE(MAX(order_datetime), '1900-01-01') FROM mydb.dbo.f_orders)
        )
        INSERT INTO mydb.dbo.f_orders
        (source_order_id, customer_id, order_amt, order_datetime, insert_timestamp)
        SELECT
            new_stg_orders.source_order_id,
            d_customers.customer_id,
            new_stg_orders.order_amt,
            new_stg_orders.order_datetime,
            GETDATE() AS insert_timestamp
        FROM new_stg_orders
        LEFT JOIN mydb.dbo.f_orders AS f_orders
            ON new_stg_orders.source_order_id = f_orders.source_order_id
        LEFT JOIN mydb.dbo.d_customers AS d_customers
            ON new_stg_orders.source_customer_id = d_customers.source_customer_id
        -- filter out duplicated order_id(s) because we implemented >= condition when creating new_stg_orders
        WHERE f_orders.source_order_id IS NULL 

        -- log ETL success
        EXEC dbo.LogETL @ETLJobName, 'Success'

        -- commit transaction
        COMMIT TRANSACTION
    END TRY
    
    BEGIN CATCH
        ROLLBACK TRANSACTION
        -- log ETL failure
        SET @ErrorMessage = ERROR_MESSAGE()
        EXEC dbo.LogETL @ETLJobName, 'Failed', @ErrorMessage
    END CATCH
END
GO

CREATE PROCEDURE dbo.RunDailyETL
AS
BEGIN
    BEGIN TRY
        EXEC dbo.LoadDimCustomers
        EXEC dbo.LoadFactOrders
        EXEC dbo.LogETL 'Daily ETL', 'Success'
    END TRY
    BEGIN CATCH
        EXEC dbo.LogETL 'Daily ETL', 'Failed'
    END CATCH
END
GO

Ta chạy store procedure RunDailyETL

EXEC dbo.RunDailyETL

Kiểm tra dữ liệu bảng etl_run_log

log_id job_name status finished_datetime error_message
1 Load Customers Dimension Table Success 2025-09-21 06:49:17.127 NULL
2 Load Orders Fact Table Success 2025-09-21 06:49:17.153 NULL
3 Daily ETL Success 2025-09-21 06:49:17.157 NULL

Kiểm tra dữ liệu bảng f_orders

source_order_id customer_id order_amt order_datetime insert_timestamp
11 3 2500 2025-01-18 08:02:00.000 2025-09-21 06:49:17.153
22 3 10000 2025-01-18 13:25:00.000 2025-09-21 06:49:17.153
33 5 3000 2025-01-18 14:08:00.000 2025-09-21 06:49:17.153
44 1 1500 2025-01-19 10:11:00.000 2025-09-21 06:49:17.153

Kiểm tra dữ liệu bảng d_customers

customer_id source_customer_id name type gender insert_timestamp
1 111 Tuan VIP Male 2025-09-21 06:49:17.123
2 222 Thanh Normal Male 2025-09-21 06:49:17.123
3 333 Trang VIP Female 2025-09-21 06:49:17.123
4 444 Tung Normal Other 2025-09-21 06:49:17.123
5 555 Linh VIP Female 2025-09-21 06:49:17.123

6 Index

6.1 Index là gì?

Index là một loại object giúp việc sắp xếp dữ liệu và tìm kiếm dữ liệu nhanh và hiệu quả hơn trong cơ sở dữ liệu quan hệ. Hiểu nôm na thì nếu coi bảng dữ liệu là 1 cuốn sách thì Index cũng giống như mục lục của cuốn sách đó. Index là 1 loại cấu trúc mà giúp ta tìm kiếm dữ liệu trong các data file (là các file mà thực sự chứa các dòng dữ liệu của bảng) nhanh hơn. Index thường được tạo trên table hoặc view.

Một số câu lệnh SQL:

Tạo index:

# Single-column index
CREATE INDEX idx_name ON table_name(column_name);

# Composite index
CREATE INDEX idx_name ON table_name(column_one_name, column_two_name);

Xoá index:

DROP INDEX index_name ON table_name

Vô hiệu hoá index:

# SQL Server
ALTER INDEX idx_name
    ON table_name
DISABLE;

Cách tạo và sử dụng index sao cho chính xác sẽ được trình bày ở một bài học riêng do tính chất phức tạp của chủ đề này.

6.2 Một số lưu ý

Trong bài học này, ta sẽ chỉ lưu ý trước tới một số nội dung mang tính chất tổng quát sau:

  • B-tree index là loại index phổ biến nhất trong các cơ sở dữ liệu quan hệ

  • Tạo và sử dụng index chỉ là một trong số những cách tối ưu performance của hệ thống cơ sở dữ liệu quan hệ. Không phải bất cứ vấn đề nào về performance cũng có thể được xử lý bằng index, thậm chí nếu tạo và sử dụng index không đúng còn làm giảm performance.

  • Một cơ sở dữ liệu được thiết kế không tốt thì dù có cố dùng index cũng khó có thể cải thiện được đáng kể performance.

  • Việc tạo và sử dụng index phải được thực hiện đúng cách và chỉ thực hiện khi cần thiết. Khi ta tạo quá nhiều index (thuật ngữ gọi là “over-indexing”) thì có thể khi đọc (read) dữ liệu thì query chạy nhanh, nhưng mỗi khi ghi (write) dữ liệu thì sẽ có rất nhiều index có liên quan phải thay đổi theo, dẫn tới giảm performance của hệ thống. Trước khi tạo index, ta cần khảo sát và monitor nhu cầu sử dụng của người dùng hay các hệ thống mà sử dụng dữ liệu trong database của ta.

  • Các query cũng cần được viết đúng cách để tận dụng được index. Một query mà khiến cho hệ quản trị cơ sở dữ liệu có thể sử dụng index để tăng tốc độ tìm kiếm thì gọi là một SARGable query (viết tắt của Search ARGument ABLE).

7 Xem thông tin về các bảng

Xem thông tin về các bảng:

SELECT * FROM information_schema.tables;

Xem danh sách bảng và các cột của mỗi bảng

SELECT * FROM information_schema.columns;

8 Xem các query đang chạy

# MySQL
SELECT * FROM information_schema.processlist WHERE COMMAND = 'Query';

# SQL Server
SELECT 
    r.sql_handle,
    r.session_id,
    r.status,
    r.command,
    r.start_time,
    r.total_elapsed_time,
    s.login_name,
    s.host_name,
    s.program_name,
    t.text AS query_text
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t