2012年8月,美国一家大型交易公司Knight Capital将其有缺陷的交易软件应用到了生产系统中。该系统使用了这些错误的配置数据,从而导致数百万笔不必要的股票交易被执行。
在短短45分钟内,这家公司损失了约4.4亿美元。Knight Capital几乎濒临倒闭,最终得到了投资者的救助,后来也被另一家公司收购了。
当Target将业务扩展到加拿大时,他们使用了一套新的供应链系统,但该系统中产品信息和库存数据存在错误。数据库中的产品信息不完整且不准确,价格、尺码和产品描述等数据都被输入错了。
库存系统显示有库存的商品,但实际上这些商品并不存在。尽管系统显示有货,顾客们在商店里却看到货架是空的。Target在加拿大市场损失了超过20亿美元,最终在2015年关闭了所有在加拿大的门店。
有一位员工这样说道:“虽然我们在纸面上拥有了一套完善的供应链系统,但实际上我们并没有准确的数据。错误的数据会导致错误的决策。”
另一个与数据相关的工程失败案例涉及“火星气候轨道器”探测器。其中一个团队使用公制单位(牛顿),而另一个团队则使用英制单位(磅力)。由于数据转换出现错误,探测器在进入火星大气层时所处的高度不正确,导致任务失败,探测器也毁坏了。这次事故造成的损失约为1.25亿美元。
在本文中,我们将深入探讨数据的真正含义、那些会悄悄破坏系统的数据错误类型、开发人员在预防这些错误方面应承担的责任,以及那些能够有效阻止错误数据进入生产环境的验证机制。
我们将会讨论的内容:
先决条件
-
对数据的基本概念有基本的了解
-
对数据结构有基本的了解
-
了解API的含义
-
了解数据库的定义及其功能
数据质量的重要性
从这些例子中就可以看出,你所使用的数据的质量确实非常重要。
根据Gartner的报告,企业们因为数据质量不佳而每年遭受约1500万美元的损失。同样的研究还表明,接近60%的企业并不清楚不良数据实际上会给他们带来多大的损失,这主要是因为这些企业根本不关注或测量数据质量问题。
IBM在2016年进行的一项研究结果更为惊人。IBM发现,由于生产率下降、系统故障以及维护成本增加,不良数据质量每年会使美国经济损失3.1万亿美元。
不良数据一直是任何企业的致命弱点。而如今,越来越多的企业依赖数据来制定战略,因此这个问题更加令人担忧。
当数据出现错误、不完整、重复或不一致时,其后果就会显现出来:错误的仪表盘会误导团队,从而导致错误的决策;而这些错误的决策又会导致错误的战略和政策的实施。
最终,企业会在财务、运营和声誉方面为此付出代价。虽然损失的钱财可以挽回,但声誉却很难恢复如初。
不良数据是如何产生的呢?
数据通常首先通过表单字段进入应用程序,因此不良数据往往也从这里开始产生。正因如此,开发人员的角色至关重要。
许多最具破坏性的数据错误并非源于恶意用户或复杂的边缘情况,而是由于系统本应避免出现的简单疏忽造成的。
但同样重要的是要认识到,数据质量问题往往在数据进入应用程序之前就已经产生了。在数据收集、测量、记录或预验证等上游环节,就可能会产生各种不准确之处。
例如,护士可能使用未校准的机械秤为患者称重,然后将错误的数据记录在纸质表格上,之后再将其输入医院系统。当这些数据最终进入应用程序时,错误就已经存在了。
这意味着,要维护数据质量,就必须同时关注上游的数据收集流程,以及开发人员所控制的系统级验证机制。
当用户界面、后端或API层允许无效、不完整、不一致或逻辑上不可能的数据进入处理流程时,企业就会面临长期性的问题。即使是一些看似微小的决策——比如允许字段留空、忽略重复数据,或者不执行验证规则——都可能引发错误,而这些错误往往要数月之后才会在报告或仪表盘中显现出来,从而导致混乱和错误的分析结果。
不良数据的代价
数据质量在任何数据处理环节都可能受到影响:无论是数据采集之前、生产阶段,还是分析过程中。
如果不良数据在用户界面阶段就被发现,那么从成本角度来看,处理这些数据的代价几乎是最低的;如果在API层被发现,代价也还算较低;但如果是在数据库中才被发现问题,那么处理成本就会增加;而如果等到数月后这些错误出现在报告或机器学习模型中,代价就会变得非常高昂,有时甚至会造成不可挽回的损失。
现代数据管理的一个关键原则是:发现不良数据的最佳且最经济的方式就是在数据源处,也就是在数据被采集之前。乔治·拉博维茨和杨宇 Sang 在1992年提出的著名“1-10-100规则”就很好地诠释了这一理念。
根据这一规则,在数据进入系统之前进行验证的成本约为1;而在数据已经进入系统后再进行纠正,成本则为10;如果错误被忽视,导致后续出现更严重的问题,那么每条记录的修复成本将为100。
正如俗话所说,“防患于未然远比事后补救有效”——尤其是在维护高质量数据方面,这一原则更为重要。
为了进一步说明我的观点,我将对那些开发人员绝不能允许出现、且必须在数据到达数据库、分析层或报告系统之前就被阻止的各种错误和疏漏进行了分类。
数据错误的类型
必填字段错误
如果你设计的表格允许用户提交一些重要字段为空的注册信息(比如姓名、地址、电子邮件地址、电话号码等),那么你就等于在让不完整的数据进入系统。
我记得在我担任数据分析师的时候,曾经分析过一个包含多栋建筑中触发的各种警报的数据集。这些警报被分为不同的类别,例如水族馆警报、入侵者警报、火灾警报以及维护警报等等。
进行这项分析的目的很简单:就是要找出哪些建筑的警报发生频率最高,从而有针对性地分配维护资源或开展调查工作。每当警报响起时,安保团队都会使用相应的软件系统来记录这些事件。每到月底,我们就可以查看所有累积起来的警报记录,并从中分析出有用的信息。
但我遇到了一个严重的数据质量问题。安全人员经常只是选择了警报类别,却没有填写发生警报的建筑物名称——而系统允许这种不完整的记录被保存到数据库中。
每条警报记录都必须对应具体的建筑物。但在分析数据时,我会看到像“20次火灾警报”这样的记录,而这些记录却没有附带任何建筑物信息。由于无法确定这些警报发生在哪里,这些数据就变得毫无用了。我别无选择,只能删除这些记录,因为它们根本无法提供任何有用的信息。
这就是数据验证不严的典型例子。如果开发人员当初设置了适当的约束条件,系统就不会允许在没有填写建筑物名称的情况下提交警报记录了。
在用户界面和后端层面都应该强制要求用户填写必填字段,这样才能从源头上防止缺失数据的出现。这些漏洞会导致数据库中出现缺失或无法使用的数据,进而迫使团队后来不得不删除这些数据或手动进行修复。
为避免这类错误,可以采用必填字段验证机制:在所有必填字段都填写完成之前,禁止用户提交表格;同时,通过内嵌的错误提示信息来直观地显示哪些字段是缺失的。
下面是一个糟糕的代码示例(没有进行任何必填字段检查):
<form id="signup">
注册>
</form>
从上面的代码片段可以看出,该表单没有强制用户填写必填字段。既没有使用HTML层面的`required`属性进行验证,也没有通过JavaScript进行检查。这种设计使得用户可以不提供必要的信息就提交表格,从而导致收集到的数据既不完整也不可靠。
从可用性和数据质量的角度来看,这种情况确实存在问题。表单的设计目的本来就是为了收集有意义且完整的信息,而“全名”和“电子邮件”这类字段通常也是必不可少的。如果不将这些字段标记为必填项,或者不通过程序进行验证,我们就可能会收到空白或无效的提交信息,进而影响存储数据的质量以及依赖这些数据的各种流程。
下面是一个改进后的示例(用户界面会阻止用户提交空表格):
<form id="signup">
注册>
</form>
在这段经过修改的代码中,我们在“名称”和“电子邮件”这两个输入元素上都添加了required属性,这样浏览器就会确保在这些字段未填写的情况下,用户无法提交表单。这是保证数据完整性、提升表单可靠性的重要措施。
此外,通过调用e.target.checkValidity()这一方法,我们现在能够在表单被提交之前对其内容进行验证。
另一个值得肯定的做法是条件性地使用e.preventDefault()函数。当表单数据无效时,这个函数会阻止表单的默认提交行为,从而避免将不完整或错误的数据发送到服务器。
格式验证错误
如果允许用户输入没有@符号的电子邮件地址、没有域名部分的电子邮件地址、包含字母的电话号码,或者格式错误的邮政编码,那么这些无效数据就会进入系统。
同样地,如果允许用户提交像“32/15/2025”这样的无效日期,或者长度不符合要求的信用卡号码,也会导致类似的问题。
这类错误会迫使数据分析师花费更多时间来清理数据——前提是这些数据还能被清理掉。而错误的输入数据往往会破坏后续的处理流程,进而增加数据清洗的工作量。
为避免这类问题,你可以使用正则表达式进行格式验证、设置输入掩码,并根据字段类型施加限制(例如,要求电话号码只能包含数字),从而确保在数据提交之前其格式是正确的。
下面是一个允许格式验证错误发生的糟糕示例:
<input id="phone" placeholder="电话号码">
<button onclick="save()">>保存</button>
<script>>
function save() {
const phone = document.getElementById("phone").value;
console.log("正在保存电话号码:", phone);
}
</script>
这段代码完全没有对电话号码的格式或结构进行任何验证。它只是简单地获取输入的值,无论这个值是否有效,都会直接将其输出到控制台。
下面是修改后的版本:
<input id="phone" placeholder="电话号码" required>
<button onclick="save()">>保存<>/button>
<script>>
function save() {
const phone = document.getElementById("phone").value;
if (!/^\d+$/.test(phone)) {
alert("电话号码必须只包含数字.");
return;
}
console.log("正在保存电话号码:", phone);
}
</script>>
在这个修改后的版本中,我们通过明确的验证规则来纠正之前的错误。在系统接受电话号码之前,会先检查输入内容是否仅由数字组成。正则表达式^\d+$可以确保数值完全由数字构成,不允许出现任何字母或符号。如果用户输入了无效的内容,这个函数就会停止执行并显示错误提示,而不会保存这些错误数据。
这种做法能够避免之前示例中出现的格式错误。代码不再盲目地接受用户输入的任何内容,而是会执行一项规则来验证这些内容是否符合电话号码应有的格式。这就是一个负责任的开发者应该做的:在使用用户输入的数据之前,先对其进行验证。
范围与限制错误
如果允许用户输入超出合理范围的值——比如负数的年龄、低于零的数量、超过100%的折扣率,或者远远超出实际可能范围的数值——那么就会导致系统接收违反业务规则的数据。这类错误会扭曲分析结果,破坏计算过程的准确性,并引发操作上的矛盾。
为了解决这些问题,你可以设置最小值/最大值限制、使用滑动条或步进按钮,以及设定明确的数字范围,以确保用户输入的值都在有效范围内。
下面是一个允许用户输入超出合理范围的错误的例子:
<input id="age" type="number">
<button onclick="submitAge()">>提交</button>
<script>>
function submitAge() {
console.log("年龄:", document.getElementById("age").value);
}
</script>
如上例所示,我们创建了一个用于输入年龄的字段,但没有设定任何限制条件。浏览器会允许用户输入任何数值——包括负数、过大的数字或是小数。JavaScript函数只是简单地读取这些值并将其记录下来,而不会检查这些年龄是否合理。
下面是一个改进后的版本:
<input id="age" type="number" min="0" max="120" required>
<button onclick="submitAge()">>提交</button>
<script>>
function submitAge() {
const ageInput = document.getElementById("age");
if (!ageInput.checkValidity()) {
alert("年龄必须在0到120之间。");
return;
}
console.log("年龄:", ageInput.value);
}
</script>>
在这个版本中,通过设置`min=”0″`和`max=”120″`属性,我们为年龄输入值明确了合理的范围。这样,只有处于这个范围内的数值才会被允许输入,从而有效防止了负数或过大的年龄这类无效数据的出现。
JavaScript函数还通过调用`checkValidity()`方法进一步强化了这一验证机制。该方法会检查用户输入的值是否满足所有设定的限制条件,包括是否为必填项以及数值范围是否合理。如果输入值不符合这些条件,函数就会阻止后续的执行,并显示警告信息,提醒用户输入的年龄必须在允许的范围内。
逻辑一致性错误
如果允许用户选择的结束日期早于开始日期,或者酒店允许的退房日期早于入住日期,又或者允许用户输入的交货日期早于订单创建时间,那么就会产生在逻辑上不可能存在的数据。同样地,如果允许用户输入的毕业年份早于他们实际入学的年份,或者让用户填写每天工作超过24小时的时长,也会出现类似的逻辑错误。
你可以通过实施跨字段验证、业务规则检查以及条件逻辑来缓解这一问题,从而确保相关字段保持一致。
下面是一个逻辑一致性错误的具体例子:
<input type="date" id="start">
<input type="date" id="end">
<button onclick="save()">>保存</button>
<script>
function save() {
console.log({
start: document.getElementById("start").value,
end: document.getElementById("end").value
});
}
</script>>
在上述代码中,根本不存在任何验证机制。虽然这些输入字段使用了type="date"属性,这使得用户能够以结构化的方式选择日期,但代码并没有强制要求用户必须填写这两个字段。这意味着用户可以将其中一个或两个日期字段留空,而save()函数仍然会正常运行并记录这些值。因此,系统最终可能会处理不完整或无意义的数据。
除了缺乏必要的验证机制外,这段代码也没有检查这两个日期之间的逻辑关系。在任何涉及开始日期和结束日期的场景中,开始日期都不应该晚于结束日期。但这段代码并没有进行这样的比较。
这就意味着用户可以选择一个晚于结束日期的开始日期,而系统也不会发出任何警告。这样一来,系统中记录的数据就会出现不一致或无法使用的情况。
此外,这个函数只是简单地记录了这些值,并没有向用户提供任何反馈。当某个字段为空,或者日期存在逻辑错误时,系统也没有任何机制来提醒用户。这种设计降低了软件的可用性,也使得用户更难以发现并纠正自己的错误。
以下是修改后的代码版本:
<input type="date" id="start" required>
<input type="date" id="end" required>
<button onclick="save()">>保存<>/button>
<script>
function save() {
const startValue = document.getElementById("start").value;
const endValue = document.getElementById("end").value;
// 额外的安全措施:检查字段是否为空(以防用户绕过必填属性)
if (!startValue || !endValue) {
alert("开始日期和结束日期都是必填项。");
return;
}
const start = new Date(startValue);
const end = new Date(endValue);
if (end < start) {
alert("结束日期不能早于开始日期。");
return;
}
console.log({ start, end });
}
</script>>
在改进后的版本中,首先,两个日期字段都添加了required属性,这样用户就无法不填写这两个字段就提交表单,从而确保了验证机制的有效性。
其次,我们增加了逻辑验证环节,以确保两个日期之间的关系是正确的。在获取到这些值之后,代码会将它们转换为Date对象,然后进行比较,以确认结束日期不会早于开始日期。如果这一条件被违反,函数会立即停止执行,并显示警告信息来告知用户错误所在。
这样就可以避免那些不一致或无法使用的日期范围被接受进来。
重复记录与数据完整性错误
当用户尝试提交已经注册过的电子邮件地址、选择已被占用的用户名,或者输入重复的员工编号或学生编号时,就会导致身份冲突和重复记录的出现。此外,如果允许用户上传不支持的文件类型、过大尺寸的文件或损坏的图片,也会引发各种问题。
当用户能够输入HTML/脚本标签(XSS攻击)、SQL注入代码或被禁止使用的特殊字符时,安全风险也会随之产生。这些问题会损害数据质量、系统完整性以及安全性。
通过实施唯一性检查、文件类型及大小验证,以及对输入内容进行清理处理,你可以有效预防这类问题,从而避免重复记录、无效上传以及恶意数据的出现。
以下是一个关于重复记录错误的示例:
<input id="email" placeholder="请输入电子邮件地址" required>
<button onclick="save()">>保存</button>
<script>
const savedEmails = [];
function save() {
const email = document.getElementById("email").value;
savedEmails.push(email);
console.log("已保存的电子邮件地址:", savedEmails);
}
</script>
在这段代码中,没有对用户输入的电子邮件地址进行重复检查,因此用户可以多次输入相同的地址。这样就会导致重复记录的产生。
以下是修改后的版本:
<input id="email" placeholder="请输入电子邮件地址" required>
<button onclick="save()">>保存<>/button>
<script>
const savedEmails = [];
function save() {
const email = document.getElementById("email").value.trim();
// 首先检查输入内容是否为空
if (!email) {
alert("请先输入电子邮件地址再保存.");
return;
}
// 然后检查该地址是否已经存在
if (savedEmails.includes(email)) {
alert("此电子邮件地址已经存在,请重新输入。");
return;
}
savedEmails.push(email);
console.log("已保存的电子邮件地址:", savedEmails);
}
</script>
在修改后的代码中,我们添加了必要的验证步骤,以确保用户不会重复输入相同的电子邮件地址。在保存之前,程序会使用`includes()`方法检查该地址是否已经存在于`savedEmails`数组中。如果找到重复的地址,程序就会停止执行并提示用户。这样就能确保每个电子邮件地址只被存储一次,从而保持数据的唯一性和完整性。
关联错误(引用完整性问题)
如果允许用户选择不属于所选国家的城市、已经不存在的产品编号、已停用的SKU号,或者在所选地区无法使用的配送方式,就可能会导致关联关系出现错误。
当用户能够从其他部门选择经理,或者选择已经被完全预定的时间段时,如果没有设置正确的角色和权限,也会出现类似的问题。这些错误会破坏表格之间的关联关系,进而导致后续的数据查询和报表出现错误。
<select id="country"> <option value="uk">>英国美国伦敦曼彻斯特 </select> <button onclick="save()">>保存 function save() { const country = document.getElementById("country").value; const city = document.getElementById("city").value; console.log("正在保存数据:", { country, city }); }从上面的代码中可以看出,我们的错误在于将“国家”和“城市”视为完全独立的字段,而实际上它们应该是相互关联的。由于无论用户选择了哪个国家,系统都会显示所有的城市选项,因此用户可能会选择一些根本不合理的组合,比如“英国”和“纽约”或“美国”和“曼彻斯特”。
此外,由于
save()函数没有进行任何验证,只是简单地将用户选择的值记录下来,因此系统会接受并存储那些本不应该存在的关联关系。这种做法会破坏这两个字段之间的逻辑联系,从而导致数据无效或不一致,进而影响整个系统的正常运行。
<select id="country" onchange="loadCities()" required> 英国 </select> <select id="city" required disabled> </select> <button onclick="save()">>保存这段改进后的代码将国家与城市的选择方式转变成了一个受控的、能够识别各种关联关系的流程,而不是简单的两个独立的下拉列表。
当用户选择一个国家后,
loadCities()函数会被执行。该函数会首先清空城市下拉列表;如果没有选择任何国家,那么城市选择框也会被禁用,这样用户就无法单独选择城市。一旦选定了一个有效的国家,城市下拉列表就会被启用,并且只会显示属于这个国家的城市名称,这一过程是通过
citiesByCountry映射来实现的。同时,这些城市名称还会被规范化处理(转换为小写并去除空格),以确保它们的一致性以及便于比较。当用户点击“保存”按钮时,
save()函数会检查是否已经选定了国家和城市。如果其中任何一个选项缺失,系统就会显示警告并停止操作。随后,系统会为所选国家重新生成一份有效的城市列表,并验证用户选择的城市确实存在于这个列表中。结构错误(下拉列表、单选按钮、枚举类型)
如果用户可以将国家输入为“U.S.A”、“USA”、“United States”或“us”,将性别输入为“male”、“Male”、“M”或“man”,或将部门名称输入为“Engineering”、“Eng”或“engineer”,那么这些不规范的输入方式可能会导致分类数据出现不一致的情况。
同样的问题也适用于货币单位(如“usd”、“USD”、“US Dollars”)、产品类别的拼写差异、状态值(如“active”、“Active”、“ACT”、“enabled”),以及布尔值(如“yes”、“Yes”、“Y”、“1”)等。
这些不一致性会使得数据分析、数据分类和报告结果变得不可靠,分析师们也因此需要花费大量时间来清理和规范这些数据。
为了确保分类数据的规范性,你应该用下拉列表、单选按钮和枚举类型来替代那些允许用户输入自由文本的字段。
下面是一个结构错误的例子:
<form id="profile"> <label>>国家>这段代码的问题在于,它只是假装在保存国家名称,但实际上并没有进行任何有效的验证或规则检查,因此这种表单设计既不可靠,也容易导致无效数据的出现。
该表单中,“国家”这一字段使用了纯文本输入框,这意味着用户可以输入任何他们想输入的内容——包括拼写错误、随机字符、不存在的国家名称,甚至什么都不输入。由于这个输入框并没有被标记为必填项,而且JavaScript也没有检查用户输入的内容是否有效,因此系统会“顺利地”保存空字符串或无意义的文本。
submit处理函数虽然阻止了表单的默认提交行为,但除了记录用户输入的内容外,并没有采取任何其他措施。因此,系统会毫无异议地接受无效、不完整或格式错误的数据。简而言之,这段代码只是收集了用户的输入信息,但却没有对其进行任何验证,也没有确保数据的正确性,更没有保护系统免受不良数据的影响。这是修改后的版本:
<form id="profile"> <label>>国家/地区> 保存 </form>>