← 中文资源

简体中文版本

数据湖与数据仓库

数据湖存储原始文件,服务多种引擎和未来问题;数据仓库存储经过建模和治理的表,支撑快速 SQL 与可信报表。

阅读英文原文

工作原理

数据湖把 Parquet、ORC、JSON、图像和日志等对象存放在廉价对象存储中,接受来自许多生产者的写入,并让消费者在读取时应用模式。目录记录数据位置,但强制约束通常较弱。数据仓库把数据加载到受管理的表中,在写入时校验模式,并提供 ACID 事务、约束、工作负载管理和成熟的 SQL 优化器。

数据湖的优势是广度:一份副本可以服务 SQL 引擎、机器学习和归档。数据仓库的优势是信任:经过整理的表格、测试过的转换,以及业务用户能够依赖的访问控制。

何时分别使用

当需要廉价保留原始数据、支持多种处理引擎,或探索模式未知的问题时,选择数据湖。当工作负载主要是 SQL、延迟和并发重要、且优先考虑受治理的可复用模型时,选择数据仓库。许多组织两者并用:原始数据落入数据湖,再把建模后的数据集市发布到仓库。

权衡

数据湖存储成本低、灵活,但默认保证较弱:小文件、缺失分区、重复事件,且不强制主键。数据仓库性能和治理更好,但每字节成本更高,对非结构化数据可能过于僵硬。把所有数据加载进仓库可能浪费钱;把所有数据留在湖里则可能浪费分析人员的时间。

示例与误解

零售商把点击事件流入对象存储,把清洗后的订单加载到仓库。数据科学家用原始事件训练模型,财务查询 fact_orders。如果直接从原始湖回答财务问题,每次查询都要重新发现去重规则和业务规则。

常见误解是数据湖自动保留历史而仓库不会。两者都需要有意的保留和版本策略;除非专门设计,任何一方都不是备份。

代码示例

-- 每个邮箱保留 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())
}