← 中文资源

简体中文版本

数据质量维度

完整性、准确性、一致性、及时性、有效性和唯一性等数据质量维度,把模糊的信任变成有阈值的可度量检查。

阅读英文原文

工作原理

每个维度都需要具体测试。完整性可以是有非空客户 ID 的订单占比;准确性把数值与可信来源比较,例如支付结算文件;一致性检查同一客户在两个系统中是否属于同一分层;及时性衡量最新记录相对预期新鲜度的年龄;有效性检查格式和域规则,例如 ISO 国家代码;唯一性检查重复键。

只有当度量附带阈值和负责人时才有用。每次加载后运行的检查可以阻止发布或开启事件;趋势线能发现单个阈值漏掉的逐渐劣化。按来源或地区切分指标,因为全局平均值可能掩盖严重损坏的分区。

选择维度

从数据支持的决策出发。欺诈模型更需要及时性和准确性,而不是可选字段的完整性;财务报告需要准确性和一致性,可以容忍批处理延迟;客户名单需要唯一性和有效性。为每个用例记录“好”的含义,而不是最大化所有指标。

权衡

更多检查提高信心,但增加计算、告警噪音和管道延迟。精确的准确性检查通常需要第二个来源,而第二个来源可能不存在。阈值太紧会误报,太松则永远抓不到真问题。质量工作与功能交付竞争资源,因此应优先处理驱动收入、合规或客户信任的数据集。

示例与误解

一张订单表完整性为 99.9%,团队因此报告高质量。但有 12% 的行晚到超过六小时,破坏了当日履约。完整性没问题,失败的是及时性。增加一个带负责人的新鲜度检查,暴露了真正的问题。

误解是质量可以用一个分数概括。适用性取决于用例,不同维度之间还会相互权衡。

代码示例

-- 每个邮箱保留 created_at 最新的一行,标记其余为重复
WITH ranked AS (
  SELECT
    id,
    email,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY email
      ORDER BY created_at DESC, id DESC
    ) AS rn
  FROM subscribers
)
SELECT id, email, created_at, (rn = 1) AS is_primary
FROM ranked
ORDER BY email, rn;

-- 一次性清扫:删除全部非首行
WITH ranked AS (
  SELECT id,
         ROW_NUMBER() OVER (
           PARTITION BY email
           ORDER BY created_at DESC, id DESC
         ) AS rn
  FROM subscribers
)
DELETE FROM subscribers s
USING ranked r
WHERE s.id = r.id
  AND r.rn > 1;

-- 上游堵漏:唯一索引阻止重复再次写入
CREATE UNIQUE INDEX CONCURRENTLY idx_subscribers_email
  ON subscribers (email);
package main

import (
	"fmt"
	"math/bits"
)

type Bitset []uint64

func NewBitset(size int) Bitset {
	if size < 0 {
		panic("negative bitset size")
	}
	return make(Bitset, (size+63)/64)
}

func (b Bitset) Set(index int) {
	b[index/64] |= uint64(1) << (uint(index) % 64)
}

func (b Bitset) Clear(index int) {
	b[index/64] &^= uint64(1) << (uint(index) % 64)
}

func (b Bitset) Has(index int) bool {
	return b[index/64]&(uint64(1)<<(uint(index)%64)) != 0
}

func (b Bitset) Union(other Bitset) Bitset {
	size := len(b)
	if len(other) > size {
		size = len(other)
	}
	out := make(Bitset, size)
	for i := range out {
		var left, right uint64
		if i < len(b) {
			left = b[i]
		}
		if i < len(other) {
			right = other[i]
		}
		out[i] = left | right
	}
	return out
}

func (b Bitset) Intersect(other Bitset) Bitset {
	size := len(b)
	if len(other) < size {
		size = len(other)
	}
	out := make(Bitset, size)
	for i := range out {
		out[i] = b[i] & other[i]
	}
	return out
}

func (b Bitset) Count() int {
	total := 0
	for _, word := range b {
		total += bits.OnesCount64(word)
	}
	return total
}

func (b Bitset) Slice() []int {
	out := make([]int, 0, b.Count())
	for wordIndex, word := range b {
		for word != 0 {
			bit := bits.TrailingZeros64(word)
			out = append(out, wordIndex*64+bit)
			word &^= uint64(1) << uint(bit)
		}
	}
	return out
}

func main() {
	active := NewBitset(80)
	for _, id := range []int{1, 5, 64, 79} {
		active.Set(id)
	}
	blocked := NewBitset(80)
	blocked.Set(5)
	blocked.Set(9)

	fmt.Println("count:", active.Count())
	fmt.Println("has 64:", active.Has(64), "has 65:", active.Has(65))
	fmt.Println("members:", active.Slice())
	fmt.Println("union count:", active.Union(blocked).Count())
	fmt.Println("intersect:", active.Intersect(blocked).Slice())

	active.Clear(64)
	fmt.Println("after clear:", active.Count())
}