Kiểm tra chất lượng dữ liệu tự động với n8n trước báo cáo

Featured Image 1200×628@2x

Báo cáo đã gửi đi, vài tiếng sau mới phát hiện doanh thu bị cộng hai lần vì một đơn hàng bị nhập trùng, hoặc một cửa hàng “biến mất” vì ai đó gõ ngày sai định dạng. Phần lớn lỗi báo cáo không nằm ở công thức mà nằm ở dữ liệu đầu vào. Bài viết này hướng dẫn bạn kiểm tra chất lượng dữ liệu tự động bằng n8n: mỗi sáng, trước giờ làm báo cáo, workflow tự quét toàn bộ bảng dữ liệu, chấm điểm chất lượng và nhắn Telegram cho bạn đúng những dòng cần sửa.

🔎 6 LOẠI LỖI WORKFLOW BẮT

▭ Dòng trống◌ Thiếu dữ liệu bắt buộc⧉ Trùng mã giao dịch✕ Sai định dạng⊘ Ngoài danh mục⏱ Dữ liệu chưa cập nhật

🩺 KẾT LUẬN MỖI SÁNG

✅ Sạch🟡 Cần xem🔴 Chưa đạt

kèm điểm chất lượng % và số dòng cần sửa

6 loại lỗi dữ liệu hay gặp nhất

Workflow trong bài kiểm tra một bảng bán hàng trên Google Sheets với các cột MaGiaoDich, NgayBan, CuaHang, SanPham, SoLuong, ThanhTien. Đây là sáu loại lỗi nó tìm:

DuLieuBanHang · ô được tô màu theo loại lỗi (dữ liệu minh họa)
A B C D E F
1 MaGiaoDich NgayBan CuaHang SanPham SoLuong ThanhTien
2 GD001 2026-10-25 Quận 1 A 2 500000
3 GD001 2026-10-25 Quận 1 A 2 500000
4            
5 GD003 25/10/2026 Q1 B 1.5 1.200.000đ
6 GD004 2026-02-30   C 1 100
7            
8 GD009 2026-10-25 Quận 3 D 1 90000
Ngày mới nhất trong bảng: 2026-10-25 · nếu sớm hơn hôm qua → ⏱ Dữ liệu chưa cập nhật

▭ Dòng trống◌ Thiếu dữ liệu bắt buộc⧉ Trùng mã giao dịch✕ Sai định dạng⊘ Ngoài danh mục⏱ Dữ liệu chưa cập nhật

▭Dòng trống

Ví dụ: Một dòng không có dữ liệu nằm giữa bảng

Nếu bỏ qua: Bộ lọc, Power Query hoặc công thức có thể dừng đọc tại đó, bỏ sót các dòng bên dưới

◌Thiếu dữ liệu bắt buộc

Ví dụ: Có thành tiền nhưng trống cột CuaHang

Nếu bỏ qua: Doanh thu không được tính vào cửa hàng nào, tổng theo cửa hàng lệch với tổng chung

⧉Trùng mã giao dịch

Ví dụ: Cùng một MaGiaoDich xuất hiện hai lần do dán file hai lần

Nếu bỏ qua: Doanh thu bị cộng dư, báo cáo đẹp hơn thực tế

✕Sai định dạng

Ví dụ: Ngày ghi 23/10/2026 thay vì 2026-10-23, thành tiền ghi “1.200.000đ” dạng chữ

Nếu bỏ qua: Dòng bị loại âm thầm khi lọc theo ngày hoặc khi cộng tổng

⊘Ngoài danh mục

Ví dụ: Cửa hàng ghi “Q1” thay vì “Quận 1”

Nếu bỏ qua: Một cửa hàng bị tách thành hai dòng trên biểu đồ

⏱Dữ liệu chưa cập nhật

Ví dụ: Ngày mới nhất trong bảng là 3 ngày trước

Nếu bỏ qua: Báo cáo “tuần này” thực ra thiếu mấy ngày gần nhất

Sáu loại lỗi này tương ứng với các tiêu chí chất lượng dữ liệu quen thuộc: đầy đủ, duy nhất, hợp lệ, nhất quán và kịp thời.

Kiến trúc workflow: 6 node

Workflow chạy lúc 7 giờ 30 sáng, trước giờ làm báo cáo. Node Kiểm tra nối cùng lúc sang hai nhánh: một nhánh ghi kết quả vào lịch sử, một nhánh gửi Telegram.

⏰

NODE 1 · LỊCH CHẠY

Schedule Trigger

Khởi chạy lúc 7 giờ 30 sáng mỗi ngày

➜ Tín hiệu bắt đầu

▦

NODE 2 · DATA

Google Sheets – Đọc dữ liệu

Đọc toàn bộ bảng bán hàng

➜ Các dòng giao dịch kèm số dòng

▦

NODE 3 · DATA

Google Sheets – Đọc danh mục

Đọc danh sách cửa hàng hợp lệ

➜ Danh mục cửa hàng

{ }

NODE 4 · KIỂM TRA

Code – Kiểm tra

Chạy 6 nhóm quy tắc, chấm điểm, soạn tin nhắn

➜ Kết quả kiểm tra và danh sách lỗi

🗂

NODE 5 · LỊCH SỬ

Google Sheets – Ghi lịch sử

Lưu kết quả mỗi lần kiểm tra

➜ Diễn biến chất lượng dữ liệu theo ngày

✈

NODE 6 · THÔNG BÁO

Telegram – Báo kết quả

Nhắn trạng thái và các dòng cần sửa

➜ Tin nhắn Telegram

✔ NÊN

Báo lỗi, sửa tận gốc

✕ KHÔNG

Sửa âm thầm trong code

💡 Nguyên tắc quan trọng nhất: báo lỗi để sửa tận gốc, không sửa âm thầm. Workflow không tự xóa dòng trùng hay tự đổi định dạng ngày, vì sửa ngầm sẽ che đi nguyên nhân và lỗi sẽ lặp lại. Thay vào đó, nó chỉ ra chính xác dòng nào, lỗi gì, để người nhập liệu sửa ngay trong file gốc và biết cách tránh lần sau.

Xây workflow từng bước

🧰 Bước 0⏰ Bước 1▦ Bước 2▦ Bước 3{ } Bước 4🗂 Bước 5✈ Bước 6

BƯỚC 0🧰 CHUẨN BỊ

Bước 0: Chuẩn bị Google Sheets và Telegram

Tạo (hoặc dùng sẵn) một file Google Sheets gồm ba sheet:

DuLieuBanHang

bảng cần kiểm tra, các cột MaGiaoDich, NgayBan, CuaHang, SanPham, SoLuong, ThanhTien. Cột NgayBan nên để dạng văn bản YYYY-MM-DD.

DanhMuc

một cột CuaHang liệt kê tên cửa hàng hợp lệ, viết đúng như muốn hiển thị trên báo cáo.

LichSuKiemTra

các cột NgayKiemTra, TongDong, SoDongLoi, DiemChatLuong, TrangThai, DuLieuMoiNhat. Sheet này do n8n tự ghi.

Trong n8n, tạo credential Google và credential Telegram (bot tạo bằng @BotFather, lệnh /newbot), ghi lại Chat ID nơi nhận báo cáo và đặt Timezone của workflow là Asia/Ho_Chi_Minh.

BƯỚC 1⏰ LỊCH CHẠY

Bước 1: Schedule Trigger – chạy 7 giờ 30 mỗi sáng

Thêm node Schedule Trigger, chọn Trigger Interval là Days, giờ 7, phút 30. Nếu bạn đã có workflow báo cáo chạy lúc 8 giờ, lịch này cho bạn nửa tiếng để sửa dữ liệu trước.

BƯỚC 2▦ DATA

Bước 2: Google Sheets – đọc dữ liệu

Thêm node Google Sheets, đặt tên Đọc dữ liệu, chọn thao tác Get row(s) in sheet, trỏ tới sheet DuLieuBanHang. Chạy thử và kiểm tra đầu ra: mỗi dòng có trường row_number là số dòng thật trên sheet. Nhờ trường này, tin nhắn báo lỗi chỉ đúng dòng cần mở ra sửa.

BƯỚC 3▦ DATA

Bước 3: Google Sheets – đọc danh mục

Thêm node Google Sheets thứ hai, đặt tên Đọc danh mục, thao tác Get row(s) in sheet, trỏ tới sheet DanhMuc. Vào Settings của node và bật Execute Once: node nhận hàng trăm item từ bước trước, nếu không bật thì nó sẽ đọc danh mục lặp lại bấy nhiêu lần.

BƯỚC 4{ } KIỂM TRA

Bước 4: Code – kiểm tra 6 nhóm quy tắc

Thêm node Code (JavaScript, chế độ Run Once for All Items), đặt tên Kiểm tra và dán đoạn code sau:

{ } Kiểm tra.jsCode node · JavaScript
1// 1. Dữ liệu cần kiểm tra và danh mục cửa hàng hợp lệ
2const rows = $('Đọc dữ liệu').all().map(i => i.json);
3const danhMuc = new Set($('Đọc danh mục').all().map(i => String(i.json.CuaHang ?? '').trim()));
4const COT = ['MaGiaoDich', 'NgayBan', 'CuaHang', 'SanPham', 'SoLuong', 'ThanhTien'];
5const NGUONG_DAT = 98; // % dòng sạch tối thiểu để coi là "Cần xem" thay vì "Chưa đạt"
6
7const loi = [];
8const sDong = (r, i) => r.row_number ?? i + 2; // số dòng thật trên Google Sheets
9const them = (r, i, loai, chiTiet) =>
10  loi.push({ dong: sDong(r, i), ma: String(r.MaGiaoDich ?? '').trim() || '(trống)', loai, chiTiet });
11const laNgay = s => DateTime.fromFormat(s, 'yyyy-MM-dd').isValid;
12
13// 2. Chạy các quy tắc cho từng dòng
14const daGap = new Map();
15rows.forEach((r, i) => {
16  const v = Object.fromEntries(COT.map(c => [c, String(r[c] ?? '').trim()]));
17
18  if (COT.every(c => v[c] === '')) { them(r, i, 'Dòng trống', 'Cả dòng không có dữ liệu'); return; }
19
20  COT.forEach(c => { if (v[c] === '') them(r, i, 'Thiếu dữ liệu', `Cột ${c} trống`); });
21
22  if (v.MaGiaoDich) {
23    if (daGap.has(v.MaGiaoDich)) them(r, i, 'Trùng', `Trùng mã với dòng ${daGap.get(v.MaGiaoDich)}`);
24    else daGap.set(v.MaGiaoDich, sDong(r, i));
25  }
26
27  if (v.NgayBan && !laNgay(v.NgayBan)) them(r, i, 'Sai định dạng', `NgayBan "${v.NgayBan}" không đúng YYYY-MM-DD`);
28  if (v.SoLuong && !(Number.isInteger(Number(v.SoLuong)) && Number(v.SoLuong) > 0))
29    them(r, i, 'Sai định dạng', `SoLuong "${v.SoLuong}" phải là số nguyên dương`);
30  if (v.ThanhTien && !(Number(v.ThanhTien) > 0))
31    them(r, i, 'Sai định dạng', `ThanhTien "${v.ThanhTien}" phải là số dương, không có chữ hay dấu chấm`);
32
33  if (v.CuaHang && !danhMuc.has(v.CuaHang)) them(r, i, 'Ngoài danh mục', `Cửa hàng "${v.CuaHang}" không có trong danh mục`);
34});
35
36// Dòng trống giữa bảng mà n8n bỏ qua khi đọc: nhận ra qua số dòng bị nhảy
37const soDong = rows.map(sDong).sort((a, b) => a - b);
38let boQua = 0;
39for (let k = 1; k < soDong.length; k++) {
40  for (let d = soDong[k - 1] + 1; d < soDong[k]; d++) {
41    loi.push({ dong: d, ma: '(trống)', loai: 'Dòng trống', chiTiet: 'Dòng trống giữa bảng' }); boQua++;
42  }
43}
44
45// 3. Quy tắc thứ 6: dữ liệu đã cập nhật đến hôm qua chưa
46const ngayHopLe = rows.map(r => String(r.NgayBan ?? '').trim()).filter(laNgay).sort();
47const moiNhat = ngayHopLe[ngayHopLe.length - 1] || '(không có)';
48const homQua = $now.minus({ days: 1 }).toFormat('yyyy-MM-dd');
49const chuaCapNhat = moiNhat === '(không có)' || moiNhat < homQua;
50
51// 4. Chấm điểm và kết luận
52const tong = rows.length + boQua;
53const soDongLoi = new Set(loi.map(l => l.dong)).size;
54const diem = tong ? Math.round((1 - soDongLoi / tong) * 1000) / 10 : 0;
55const trangThai = soDongLoi === 0 && !chuaCapNhat ? 'Sạch'
56  : diem >= NGUONG_DAT && !chuaCapNhat ? 'Cần xem' : 'Chưa đạt';
57const theoLoai = loi.reduce((m, l) => { m[l.loai] = (m[l.loai] || 0) + 1; return m; }, {});
58
59// 5. Soạn tin nhắn Telegram (tối đa 15 lỗi đầu tiên)
60const BIEU_TUONG = { 'Sạch': '✅', 'Cần xem': '🟡', 'Chưa đạt': '🔴' };
61const dong = [
62  `🧪 Kiểm tra dữ liệu ${$now.toFormat('dd/MM/yyyy HH:mm')}`,
63  `${BIEU_TUONG[trangThai]} ${trangThai} · Điểm chất lượng: ${diem}%`,
64  `Tổng ${tong} dòng · ${soDongLoi} dòng có lỗi`,
65  ...Object.entries(theoLoai).map(([k, n]) => `- ${k}: ${n}`),
66  `Dữ liệu mới nhất: ${moiNhat}${chuaCapNhat ? ' ⚠️ chưa cập nhật đến hôm qua' : ''}`
67];
68if (loi.length) {
69  loi.sort((a, b) => a.dong - b.dong);
70  dong.push('', 'Cần sửa:', ...loi.slice(0, 15).map(l => `• Dòng ${l.dong} (${l.ma}): ${l.loai} – ${l.chiTiet}`));
71  if (loi.length > 15) dong.push(`… và ${loi.length - 15} lỗi khác`);
72} else {
73  dong.push('', 'Không có lỗi, sẵn sàng làm báo cáo.');
74}
75
76return [{ json: {
77  NgayKiemTra: $now.toFormat('yyyy-MM-dd HH:mm'), TongDong: tong, SoDongLoi: soDongLoi,
78  DiemChatLuong: diem, TrangThai: trangThai, DuLieuMoiNhat: moiNhat, tinNhan: dong.join('\n')
79} }];

Điểm chất lượng là tỷ lệ dòng không có lỗi. Kết quả có ba mức: Sạch (không lỗi, dữ liệu đã cập nhật), Cần xem (còn vài lỗi nhỏ, điểm từ 98% trở lên) và Chưa đạt (nhiều lỗi hoặc dữ liệu chưa cập nhật). Bạn đổi ngưỡng bằng hằng số NGUONG_DAT ở đầu code. Code dùng DateTime (thư viện Luxon có sẵn trong node Code) để bắt cả những ngày không tồn tại như 2026-02-30.

✅ Sạch🟡 Cần xem🔴 Chưa đạt

BƯỚC 5🗂 LỊCH SỬ

Bước 5: Google Sheets – ghi lịch sử kiểm tra

Thêm node Google Sheets, đặt tên Ghi lịch sử, thao tác Append row in sheet, trỏ tới sheet LichSuKiemTra. Chọn Mapping Column Mode là Map Each Column Manually và điền sáu cột bằng biểu thức tương ứng, ví dụ cột DiemChatLuong là {{ $json.DiemChatLuong }}. Sau vài tuần, sheet này cho bạn thấy chất lượng dữ liệu đang tốt lên hay xấu đi.

BƯỚC 6✈ THÔNG BÁO

Bước 6: Telegram – báo kết quả

Từ node Kiểm tra, kéo thêm một đường nối sang node Telegram, thao tác Send Message: Chat ID là nơi nhận báo cáo, Text là {{ $json.tinNhan }}, và tắt Append n8n Attribution trong Additional Fields. Chạy thử bằng Execute workflow, rồi kích hoạt workflow để lịch 7 giờ 30 có hiệu lực.

Đọc kết quả kiểm tra

Với một bảng có vài lỗi, tin nhắn Telegram trông như sau (dữ liệu minh họa):

✦Bot kiểm tra dữ liệu
gửi lúc 07:30 · dữ liệu minh họa
✦

🧪 Kiểm tra dữ liệu 26/10/2026 07:30
🔴 Chưa đạt · Điểm chất lượng: 28.6%
Tổng 7 dòng · 5 dòng có lỗi
– Trùng: 1
– Dòng trống: 2
– Sai định dạng: 4
– Ngoài danh mục: 1
– Thiếu dữ liệu: 1
Dữ liệu mới nhất: 2026-10-25

Cần sửa:
• Dòng 3 (GD001): Trùng – Trùng mã với dòng 2
• Dòng 4 ((trống)): Dòng trống – Cả dòng không có dữ liệu
• Dòng 5 (GD003): Sai định dạng – NgayBan "25/10/2026" không đúng YYYY-MM-DD
• Dòng 5 (GD003): Sai định dạng – SoLuong "1.5" phải là số nguyên dương
• Dòng 5 (GD003): Sai định dạng – ThanhTien "1.200.000đ" phải là số dương, không có chữ hay dấu chấm
• Dòng 5 (GD003): Ngoài danh mục – Cửa hàng "Q1" không có trong danh mục
• Dòng 6 (GD004): Thiếu dữ liệu – Cột CuaHang trống
• Dòng 6 (GD004): Sai định dạng – NgayBan "2026-02-30" không đúng YYYY-MM-DD
• Dòng 7 ((trống)): Dòng trống – Dòng trống giữa bảng

Bot dữ liệu

Người nhập liệu chỉ cần mở sheet, đi đến đúng số dòng và sửa. Một dòng có thể có nhiều lỗi cùng lúc, nên số lỗi theo loại có thể lớn hơn số dòng có lỗi.

Nối với workflow báo cáo

Kiểm tra chỉ có ích khi nó chặn được báo cáo sai. Có hai cách kết hợp với workflow trong bài Tự động hóa báo cáo tuần với n8n và AI:

⏱ Chạy trước, sửa tay

Workflow kiểm tra chạy lúc 7 giờ 30, báo cáo chạy lúc 8 giờ; người phụ trách sửa lỗi trong nửa tiếng

Phù hợp khi: Nhóm nhỏ, người nhập liệu phản hồi nhanh

🛑 Chặn tự động

Chèn node Kiểm tra vào đầu workflow báo cáo, thêm node IF: nếu TrangThai là “Chưa đạt” thì dừng và gửi cảnh báo thay cho báo cáo

Phù hợp khi: Báo cáo gửi cho nhiều người, sai một lần là mất uy tín

Mở rộng quy tắc kiểm tra

📈 Giá trị bất thường

đánh dấu giao dịch có ThanhTien lớn hơn nhiều lần mức thường gặp, vì đó có thể là lỗi gõ thừa số 0.

🗂 Lưu chi tiết từng lỗi

trả về mỗi lỗi một item và ghi vào một sheet riêng để theo dõi lỗi nào hay lặp lại, ai hay nhập sai.

✦ Dùng Claude cho cột chữ tự do

với tên sản phẩm viết nhiều kiểu khác nhau, có thể nhờ Claude gợi ý cách gộp về một tên chuẩn, rồi người phụ trách duyệt trước khi áp dụng.

🛡 Chặn lỗi ngay từ lúc nhập

dùng Data validation của Google Sheets (danh sách thả xuống cho CuaHang, kiểu ngày cho NgayBan, số dương cho ThanhTien) để giảm số lỗi workflow phải báo.

Checklist trước khi kích hoạt

☑ TRƯỚC KHI KÍCH HOẠT

✓Sheet DanhMuc liệt kê đủ cửa hàng, viết đúng chính tả
✓Node Đọc danh mục đã bật Execute Once
✓Đã cố ý tạo vài dòng lỗi (trùng mã, ngày sai, ô trống) và thấy workflow báo đúng số dòng
✓Múi giờ workflow là Asia/Ho_Chi_Minh để quy tắc “cập nhật đến hôm qua” tính đúng
✓Sheet LichSuKiemTra nhận đủ sáu cột sau lần chạy thử
✓Lịch kiểm tra chạy trước lịch báo cáo ít nhất 30 phút

6/6 mục đạt → bật lịch 7:30 mỗi sáng

📅 Muốn học bài bản cách làm sạch, xử lý và phân tích dữ liệu với Power BI, SQL, Python và AI? Xem Lịch khai giảng tháng 10/2026 các khóa Power BI, SQL, Python, AI tại MCNA.

Câu hỏi thường gặp (FAQ)

?Sao không tự động xóa dòng trùng cho nhanh?

Vì workflow không biết dòng nào đúng: hai dòng cùng mã có thể khác thành tiền, và việc xóa nhầm còn nguy hiểm hơn giữ trùng. Báo lỗi kèm số dòng để người nắm nghiệp vụ quyết định là cách an toàn và giúp lỗi không lặp lại.

?Có nên dùng AI để kiểm tra dữ liệu không?

Với các quy tắc rõ ràng như trống, trùng, sai định dạng, code nhanh hơn, rẻ hơn và cho kết quả giống nhau mỗi lần. AI phù hợp với phần mờ hơn như gợi ý chuẩn hóa tên sản phẩm viết nhiều kiểu, và kết quả nên được người duyệt lại.

?Dữ liệu nằm trong Excel trên OneDrive hoặc trong SQL thì sao?

Thay hai node Google Sheets đọc dữ liệu bằng node Microsoft Excel 365 hoặc node database tương ứng. Với SQL, nhiều quy tắc còn có thể viết ngay bằng câu truy vấn, ví dụ GROUP BY MaGiaoDich HAVING COUNT(*) > 1 để tìm mã trùng.

?Bảng có hàng chục nghìn dòng có chạy được không?

Được, code chỉ duyệt qua bảng một lần. Tin nhắn Telegram chỉ hiện 15 lỗi đầu tiên để không quá dài; khi lỗi nhiều, hãy lưu chi tiết ra sheet riêng như phần mở rộng.

?Không biết JavaScript có làm được không?

Được. Bạn dùng nguyên code trong bài, chỉ cần sửa danh sách cột trong hằng số COT cho khớp với bảng của mình, hoặc nhờ Claude thêm quy tắc mới bằng cách mô tả bằng lời.

Kết luận

Kiểm tra chất lượng dữ liệu tự động là lớp bảo vệ rẻ nhất cho mọi báo cáo: 6 node n8n, một đoạn code và một bot Telegram là đủ để bắt dòng trống, dòng trùng, sai định dạng và dữ liệu cũ trước khi chúng lọt vào báo cáo. Chìa khóa là báo đúng dòng cần sửa thay vì sửa âm thầm, chạy trước giờ làm báo cáo, và theo dõi điểm chất lượng theo thời gian. Dữ liệu sạch thì báo cáo mới đáng tin.

Nếu bạn muốn học bài bản cách xử lý, phân tích dữ liệu và tự động hóa với AI, các khóa học tại MCNA Technology School sẽ đồng hành cùng bạn. Đọc thêm: Tự động hóa báo cáo tuần với n8n và AI và AI Automation cho Data Analyst.

👉 Xem lịch khai giảng tháng 10/2026: Power BI, SQL, Python, AI – MCNA

Thông tin tác giả

Võ Ngọc Vương – Data Analyst tại MCNA Technology School

MCNA Technology School – Đơn vị tiên phong đào tạo AI, Big Data, Business Intelligence tại Việt Nam. Chuyên sâu về Power BI, Python, SQL và Data Analytics — đối tác đào tạo của 300+ doanh nghiệp.

Đối tác đào tạo: Viettel Global, Masan Group, Techcombank, VPBank, Daikin.

→ Chỉ mục