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&gt>

在改进后的版本中,首先,两个日期字段都添加了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">>英国美国伦敦曼彻斯特
  

从上面的代码中可以看出,我们的错误在于将“国家”和“城市”视为完全独立的字段,而实际上它们应该是相互关联的。由于无论用户选择了哪个国家,系统都会显示所有的城市选项,因此用户可能会选择一些根本不合理的组合,比如“英国”和“纽约”或“美国”和“曼彻斯特”。

此外,由于save()函数没有进行任何验证,只是简单地将用户选择的值记录下来,因此系统会接受并存储那些本不应该存在的关联关系。这种做法会破坏这两个字段之间的逻辑联系,从而导致数据无效或不一致,进而影响整个系统的正常运行。

<select id="country" onchange="loadCities()" required>
  

这段改进后的代码将国家与城市的选择方式转变成了一个受控的、能够识别各种关联关系的流程,而不是简单的两个独立的下拉列表。

当用户选择一个国家后,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>>国家/地区>