Hai sự kiện mở màn hình có thể cùng thời điểm và cùng loại, nhưng vẫn là hai lần xảy ra riêng biệt. Nếu hợp nhất log bằng UNION sau khi bỏ cột ID, bạn có thể vô tình biến chúng thành một dòng. Một báo cáo ít dòng hơn chưa chắc đã là dữ liệu được làm sạch đúng.
Bài đối chiếu PostgreSQL 17 và tài liệu SQLite ngày 28/10/2026. Fixture được chạy bằng SQLite CLI 3.54.0 trong database bộ nhớ; chưa chạy trên PostgreSQL hoặc đo hiệu năng. Mục tiêu là chọn đúng ngữ nghĩa trước khi tối ưu truy vấn.
1. Tạo bốn sự kiện có dữ liệu hiển thị trùng nhau
Mở sqlite3 :memory: và chạy các đoạn SQL theo thứ tự trong cùng phiên:
CREATE TABLE web_events (id INTEGER PRIMARY KEY, at TEXT, kind TEXT);
CREATE TABLE mobile_events (id INTEGER PRIMARY KEY, at TEXT, kind TEXT);
INSERT INTO web_events VALUES
(1, '2026-10-27T08:00:00Z', 'open'),
(2, '2026-10-27T08:00:00Z', 'open');
INSERT INTO mobile_events VALUES
(1, '2026-10-27T08:00:00Z', 'open'),
(2, '2026-10-27T08:01:00Z', 'close');
Mỗi bảng có hai ID khác nhau. ID số 1 ở web và ID số 1 ở mobile thuộc hai nguồn, nên không tự tạo thành một định danh toàn cục. Cột thời gian dạng text ở đây dùng cùng định dạng UTC để fixture dễ đọc; schema production cần kiểu thời gian và ràng buộc phù hợp hệ quản trị.
2. UNION loại trùng ở kết quả đã chiếu cột
SELECT at, kind FROM web_events
UNION
SELECT at, kind FROM mobile_events
ORDER BY at, kind;
Kết quả có hai dòng: một open lúc 08:00 và một close lúc 08:01. Dữ liệu hiển thị của ba sự kiện open giống hệt nhau sau khi chỉ chọn hai cột, nên chúng được gộp thành một.
Đây là hành vi đúng của UNION. Nó không biết ba dòng đại diện cho ba hành động riêng hay ba bản sao của cùng hành động. Ý nghĩa đó phải đến từ hợp đồng dữ liệu và các cột bạn giữ lại.
Nếu yêu cầu là tập các cặp thời điểm–loại duy nhất, kết quả hai dòng có thể chính là điều cần tìm. Nếu yêu cầu là đếm số sự kiện, làm mất số lần xuất hiện trước khi đếm sẽ đổi kết quả nghiệp vụ.
3. Giữ số lần xuất hiện bằng UNION ALL
WITH combined AS (
SELECT at, kind FROM web_events
UNION ALL
SELECT at, kind FROM mobile_events
)
SELECT kind, COUNT(*) AS events
FROM combined
GROUP BY kind
ORDER BY kind;
Kết quả là close = 1 và open = 3. Nếu thay UNION ALL bằng UNION trong CTE, open chỉ còn 1. Cả hai câu SQL đều chạy, nhưng đang trả lời hai câu hỏi khác nhau.
UNION ALL cũng giữ dòng lặp có trong chính một nguồn. Nó không phải một thao tác chống ghi trùng. Khi hệ thống có retry gửi cùng sự kiện nhiều lần, cần một định danh sự kiện và chính sách khử trùng riêng, không chỉ đổi toán tử hợp nhất.
4. Mang theo nguồn và ID khi cần truy vết
SELECT 'web' AS source, id, at, kind FROM web_events
UNION ALL
SELECT 'mobile' AS source, id, at, kind FROM mobile_events
ORDER BY source, id;
Câu này giữ cả bốn sự kiện cùng định danh ghép (source, id). Nó giúp bạn quay lại nguồn khi kiểm tra một dòng báo cáo. Nếu thêm các cột này rồi dùng UNION, tiêu chuẩn “trùng” cũng thay đổi vì toàn bộ hàng kết quả đã khác.
Đừng khử trùng theo vài cột dễ hiển thị chỉ vì chúng giống nhau. Hãy thống nhất khóa nghiệp vụ, cách xử lý event gửi lại và việc hai nguồn có thể cùng mô tả một hành động hay không. Những quyết định đó nằm ngoài khả năng suy luận tự động của UNION.
Kiểm tra hình dạng và thứ tự kết quả
Hai nhánh cần trả cùng số cột, theo vị trí và có kiểu dữ liệu tương thích. Viết danh sách cột rõ ràng giúp tránh phụ thuộc vào SELECT * khi schema của một nguồn thay đổi. Tên cột giống nhau cũng không cứu được việc hai nhánh đặt sai thứ tự cột.
Đặt ORDER BY ở tầng kết quả chung khi cần thứ tự ổn định. Không dựa vào việc nhánh web được viết trước để giả định mọi dòng web luôn xuất hiện trước. Trong ví dụ cuối, thứ tự được nêu trực tiếp bằng source và id.
Lỗi thường gặp và giới hạn
Nếu tổng số sự kiện giảm, kiểm tra phép chiếu cột và toán tử hợp nhất trước khi sửa COUNT. Thử dữ liệu trùng trong một nhánh, trùng giữa hai nhánh và cùng ID ở hai nguồn. Những fixture này bộc lộ khác biệt rõ hơn dữ liệu mà mọi hàng đều duy nhất.
UNION cần xử lý loại trùng trong khi UNION ALL không yêu cầu bước đó theo ngữ nghĩa, nhưng bài chưa đo thời gian hoặc bộ nhớ. Hãy xem execution plan trên dữ liệu thật nếu hiệu năng là vấn đề. Không chọn UNION ALL chỉ vì nghe nói nhanh hơn khi nghiệp vụ thực sự cần một tập duy nhất.


