数据工程实战课程 · 数据存储阶段
分区和分桶通过物理切分数据实现查询时数据裁剪,可提升10-100倍查询性能。
# 分区与分桶引擎
import hashlib, time, random, json
from collections import defaultdict
class PartitionedTable:
def __init__(self, name, partition_cols):
self.name = name; self.partition_cols = partition_cols
self.partitions = defaultdict(list); self.stats = {}
def insert(self, records):
for r in records:
key = tuple(r.get(c,'null') for c in self.partition_cols)
self.partitions[key].append(r)
self._update()
def _update(self):
self.stats = {str(k):len(v) for k,v in self.partitions.items()}
def scan(self, filter=None):
result = []; skipped = 0; scanned = 0
for key, records in self.partitions.items():
if filter:
match = all(key[i] == filter.get(c, key[i]) for i, c in enumerate(self.partition_cols))
if not match: skipped += len(records); continue
result.extend(records); scanned += len(records)
return result, {'scanned':scanned,'skipped':skipped}
class BucketedTable:
def __init__(self, name, bucket_col, num_buckets=10):
self.name = name; self.bucket_col = bucket_col; self.num_buckets = num_buckets
self.buckets = defaultdict(list)
def _bid(self, v): return int(hashlib.md5(str(v).encode()).hexdigest(),16)%self.num_buckets
def insert(self, records):
for r in records: self.buckets[self._bid(r.get(self.bucket_col,''))].append(r)
def bucket_join(self, other, join_key):
results = []; comparisons = 0
for bid in self.buckets:
if bid in other.buckets:
for r1 in self.buckets[bid]:
for r2 in other.buckets[bid]:
comparisons += 1
if r1.get(join_key) == r2.get(join_key): results.append({**r1,**{f'r_{k}':v for k,v in r2.items()}})
return results, comparisons
def stats(self):
total = sum(len(v) for v in self.buckets.values())
mx = max((len(v) for v in self.buckets.values()), default=0)
return {'buckets':self.num_buckets,'records':total,'max_bucket':mx,'skew':round(mx/(total/self.num_buckets),2) if total else 0}
regions = ['华东','华南','华北','西南','东北']
pt = PartitionedTable('orders', ['region'])
records = [{'id':i,'amount':round(random.uniform(10,5000),2),'region':random.choice(regions),'date':f"2024-{random.randint(1,12):02d}-{random.randint(1,28):02d}"} for i in range(10000)]
pt.insert(records)
print("分区表:")
start = time.perf_counter()
result, meta = pt.scan({'region':'华东'})
t1 = (time.perf_counter()-start)*1000
start = time.perf_counter()
full, _ = pt.scan()
t2 = (time.perf_counter()-start)*1000
print(f" 裁剪(华东): {len(result)}条 {t1:.2f}ms | 全表: {len(full)}条 {t2:.2f}ms | 加速{t2/t1:.1f}x")
print(f" 分区分布: {dict(list(pt.stats.items())[:5])}")
bt1 = BucketedTable('orders','customer_id',20)
bt2 = BucketedTable('customers','customer_id',20)
bt1.insert([{'id':i,'amount':round(random.uniform(10,5000),2),'customer_id':f'CUST_{random.randint(1,1000)}'} for i in range(5000)])
bt2.insert([{'customer_id':f'CUST_{i}','name':f'客户_{i}','tier':random.choice('ABC')} for i in range(1000)])
join_result, comps = bt1.bucket_join(bt2, 'customer_id')
print(f"\n分桶: orders={bt1.stats()}, customers={bt2.stats()}")
print(f"分桶Join: {len(join_result)}条, {comps}次比较")
print("✅ 验证通过 - 分区分桶引擎运行正常")
🎁 下一课预告:元数据管理!
| 策略 | 原理 | 节省比例 |
|---|---|---|
| 分层存储 | 热SSD/温HDD/冷对象存储 | 50-80% |
| 数据压缩 | 列式存储高压缩比 | 60-90% |
| 生命周期 | 自动归档删除 | 30-60% |
| 降采样 | 历史数据低精度 | 70-95% |
在生产环境中,我们经常遇到教科书不会告诉你的问题。以下是常见的实战经验和解决方案:
| 指标 | 告警阈值 | 监控方式 |
|---|---|---|
| 管道延迟 | 超过SLA 120% | 端到端时间戳追踪 |
| 数据量偏差 | 日均偏差超过30% | 与历史同期对比 |
| 错误率 | 超过0.1% | 错误计数/总记录数 |
| 数据新鲜度 | 超过SLA 150% | 最新数据时间戳 |
| 资源利用率 | CPU持续>80% | 系统监控 |
A: 根据业务延迟要求决定:秒级用流处理、分钟级用微批、小时级用批处理。大部分场景批处理足够,流处理用于实时业务场景(风控、监控、推荐)。
A: 不会完全取代,但边界在模糊。湖仓一体(如Iceberg/Delta Lake)正在融合两者优势。未来趋势是统一存储+多引擎计算。
A: 先用云原生服务(BigQuery/Snowflake + Fivetran + dbt + Looker),验证业务后再考虑自建。过早优化是万恶之源。
A: 用钱说话:数据质量问题的直接经济损失(错误决策、合规罚款、重复劳动)远大于治理投入。量化问题是第一步。
| 角色 | 职责 | 交付物 |
|---|---|---|
| 数据工程师 | 管道开发与维护 | 数据管道、基础设施 |
| 数据分析师 | 数据分析与报表 | SQL查询、BI报表 |
| 数据科学家 | 模型与实验 | ML模型、实验报告 |
| 数据产品经理 | 需求与优先级 | PRD、数据产品规划 |
| 数据管家 | 数据质量与治理 | 质量规则、数据字典 |