---
domain: "data-engineering"
title: "Data Quality Fix: Always Keep Three Layers (DB + ETL + Query)"
status: "draft"
verification: "metadata-normalized"
{"title": "Data Quality Fix: Always Keep Three Layers (DB + ETL + Query)", "domain": "data-engineering", "tags": ["data-quality", "etl", "sql", "normalization", "defense-in-depth"], "status": "published", "confidence": "0.95", "created": "2026-08-06", "updated": "2026-08-06", "source": "b2-robot-utilization project — FE/TGO line name normalization", "verified_date": "2026-08-06", "domain_expert": ""}
---
ETL pipeline derives identifiers from source filenames (e.g., robot_name from Excel sheet names). Filenames are inherently inconsistent — the same physical robot appears as FE135R01, FE66135R01, UB&FE135R01, and FE_处理后数据135R01 depending on which file it came from.
A single-point fix (e.g., only fixing the database) leaves the system vulnerable to regression when new data arrives through the same broken ETL.
No normalization layer exists between raw filename parsing and database insertion. The ETL trusts filenames as-is, and downstream queries assume identifiers are consistent.
The failure pattern:
- ETL inserts inconsistent names → database has duplicates
- Query
GROUP BY robot_name→ inflated counts (11 robots instead of 1) - Dashboard shows wrong numbers → user loses trust
Apply fixes at three layers, in order of permanence:
Add a normalization function that runs at insert time:
# common.py — single source of truth
ROBOT_NAME_PREFIXES = [
('TGO_FEN_处理后数据', 'TGO'),
('FEN&TGO', 'TGO'),
('TGO&FEN', 'TGO'),
('TGO FEN', 'TGO'),
('FE_处理后数据', 'FE'),
('UB&FE', 'FE'),
('FE66', 'FE'),
]
def normalize_robot_name(name):
for prefix, replacement in ROBOT_NAME_PREFIXES:
if name.startswith(prefix):
return replacement + name[len(prefix):]
return nameIn the ETL script:
from common import normalize_robot_name
def process_record(rec):
robot_name = normalize_robot_name(rec['robot_name'])
# ... insert into DBRun a one-time migration script that normalizes existing data:
# fix_robot_names.py — uses the SAME normalize function as ETL
from common import normalize_robot_name
for old_name in dirty_names:
new_name = normalize_robot_name(old_name)
if old_name != new_name:
conn.execute("UPDATE table SET robot_name=? WHERE robot_name=?",
(new_name, old_name))Keep a CASE WHEN in SQL as a defense-in-depth fallback:
CASE WHEN t.line IN ('FEN&TGO', 'FE66', 'FE_处理后数据') THEN 'FE'
ELSE t.line END as normalized_lineThis catches any edge case that slips through Layers 1 and 2.
-- Before fix: 11 "different" robots (all the same physical robot)
SELECT robot_name, COUNT(*) FROM robot_timestamps
WHERE line='FE' GROUP BY robot_name;
-- FE135R01: 635, FE66135R01: 63, UB&FE135R01: 49, ...
-- After fix: 1 robot with merged data
SELECT robot_name, COUNT(*) FROM robot_timestamps
WHERE line='FE' GROUP BY robot_name;
-- FE135R01: 1194- The normalization function MUST be shared between ETL, migration script, and any other code that touches identifiers. If you copy-paste the prefix list, it will diverge.
- Prefix matching must be ordered by length (longest first) to avoid short-prefix false matches (e.g.,
FEmatching beforeFE66). - SELECT queries are not affected by this issue — only
GROUP BYandJOINon the inconsistent column produce wrong results. - This pattern applies to any ETL that derives identifiers from filenames, not just robot names. Common cases: station names, line names, product codes.
- The
CASE WHENin Layer 3 can be removed once Layer 1 is proven stable across multiple ETL runs.