探秘新一代向量存储格式Lance-format (十三) 数据更新与 Schema 演化

第13章:数据更新与 Schema 演化

🎯 核心概览

数据更新和 Schema 演化是现实系统的关键需求。Lance 支持无重写的列添加、类型转换和回填机制。


📊 第一部分:列的添加与删除

列添加(零成本)

python 复制代码
import lance

dataset = lance.open("users.lance")

# 添加新列:location(有默认值)
dataset.add_column("location", "default_city", default_value="Unknown")

# 内部机制:
# 1. 在 Schema 中添加新列定义
# 2. 记录:新列+默认值信息
# 3. 不修改现有数据文件
# 4. 读取时自动填充默认值

# 性能:O(1),只修改元数据

实现细节

rust 复制代码
impl Dataset {
    pub async fn add_column(
        &mut self,
        column_name: String,
        data_type: DataType,
        default_value: Option<ScalarValue>,
    ) -> Result<()> {
        // 步骤 1:验证列名不存在
        if self.schema.contains(&column_name) {
            return Err(Error::ColumnAlreadyExists(column_name));
        }
        
        // 步骤 2:在 Schema 中添加新列
        let new_field = Field::new(&column_name, data_type, true);
        self.schema.push(new_field);
        
        // 步骤 3:记录列的默认值
        let mut new_manifest = self.manifest.clone();
        new_manifest.metadata.insert(
            format!("column:{}_default", column_name),
            default_value.serialize()?,
        );
        
        // 步骤 4:原子提交
        self.commit(new_manifest).await?;
        
        Ok(())
    }
}

读取时的处理

rust 复制代码
pub fn read_batch_with_schema_evolution(
    batch: RecordBatch,
    evolved_schema: &Schema,
) -> Result<RecordBatch> {
    let mut columns = batch.columns().to_vec();
    
    // 为新列添加默认值
    for field in evolved_schema.fields() {
        if !batch.schema().contains(field.name()) {
            // 新列不在原始批次中
            let default_value = field.metadata.get("default")?;
            let default_array = create_array_with_value(
                default_value,
                batch.num_rows(),
                &field.data_type,
            )?;
            columns.push(Arc::new(default_array));
        }
    }
    
    RecordBatch::try_new(evolved_schema.into(), columns)
}

列删除

python 复制代码
# 删除列:不删除数据文件
dataset.drop_column("old_column")

# 内部:
# 1. 在 Schema 中标记列为已删除
# 2. 创建新 Manifest
# 3. 扫描时跳过此列
# 4. 存储文件不变(向后兼容)

# 性能:O(1)

🔄 第二部分:类型转换

兼容的转换

go 复制代码
可以零拷贝或廉价转换的类型:

int32 → int64
✓ 可以:符号扩展

float32 → float64
✓ 可以:精度提升

string → binary
✓ 可以:编码转换

但不可以逆转转换:
int64 → int32
✗ 范围可能溢出

float64 → float32
✗ 精度丢失

类型转换实现

rust 复制代码
impl Dataset {
    pub async fn alter_column_type(
        &mut self,
        column_name: String,
        new_type: DataType,
    ) -> Result<()> {
        // 步骤 1:验证转换可行
        if !can_convert(&self.schema.field(&column_name).data_type, &new_type) {
            return Err(Error::IncompatibleTypeConversion);
        }
        
        // 步骤 2:如果是廉价转换,只修改 Schema
        if is_cheap_conversion(&self.schema.field(&column_name).data_type, &new_type) {
            self.schema.field_mut(&column_name).data_type = new_type;
            let new_manifest = self.manifest.clone();
            return self.commit(new_manifest).await;
        }
        
        // 步骤 3:昂贵转换,需要回填
        self.backfill_column_type(&column_name, &new_type).await?;
        
        Ok(())
    }
}

💫 第三部分:回填(Backfill)机制

回填流程

makefile 复制代码
场景:添加必需列(非空,无默认值)

步骤 1:扫描现有数据,提取值
old_data: [row_0, row_1, ..., row_N]
新列需要计算值: compute_value(row_i) → value_i

步骤 2:生成新列数据
new_column: [value_0, value_1, ..., value_N]

步骤 3:创建新 Fragment
包含:旧列 + 新列数据

步骤 4:更新 Manifest
指向新 Fragment

步骤 5:后台清理
删除旧 Fragment(可选)

性能:取决于列的计算复杂度

实现示例

rust 复制代码
impl Dataset {
    pub async fn backfill_column_type(
        &mut self,
        column_name: &str,
        new_type: &DataType,
    ) -> Result<()> {
        // 步骤 1:扫描原始列
        let old_column = self.scan()
            .select(vec![column_name.to_string()])
            .try_into_stream()
            .await?
            .collect::<Vec<_>>()
            .await;
        
        // 步骤 2:转换
        let converted_column = self.convert_column(
            old_column,
            &self.schema.field(column_name).data_type,
            new_type,
        ).await?;
        
        // 步骤 3:添加回填标记到 Schema
        self.schema.field_mut(column_name).data_type = new_type.clone();
        
        // 步骤 4:创建新的 Manifest(引用转换后的数据)
        let mut new_manifest = self.manifest.clone();
        new_manifest.metadata.insert(
            format!("column:{}_backfilled", column_name),
            "true".to_string(),
        );
        
        // 步骤 5:提交
        self.commit(new_manifest).await?;
        
        Ok(())
    }
}

📊 Schema 演化的实际案例

案例 1:电商产品表演化

go 复制代码
初始版本(v0):
{
    product_id: int64,
    name: string,
    price: float32,
}

v1:添加分类
dataset.add_column("category", string, default="general")

v2:添加评分
dataset.add_column("rating", float32, default=0.0)

v3:添加库存
dataset.add_column("stock", int32, default=0)

成本分析:
✓ 每次操作都是 O(1)
✓ 无数据重写
✓ 支持时间旅行(访问 v0 时自动填充默认)

读取时间旅行:
dataset_v0 = lance.open("products.lance", version=0)
# Schema:{product_id, name, price}

dataset_v3 = lance.open("products.lance", version=3)
# Schema:{product_id, name, price, category=default, rating=default, stock=default}

案例 2:数据类型升级

makefile 复制代码
初始版本:
user_count: int32

v1:升级到 int64(支持更大的数字)
dataset.alter_column_type("user_count", int64)

# Lance 自动:
# 1. 检测这是廉价转换(符号扩展)
# 2. 只修改 Schema
# 3. 读取时自动转换

成本:O(1) - 无数据重写

🏗️ 第四部分:批量更新

行级别更新

python 复制代码
# 更新单行的单个值
dataset.update(
    row_id=12345,
    column="status",
    value="inactive"
)

# 内部:
# 1. 找到 row_id 对应的 Fragment
# 2. 加载该 Fragment
# 3. 修改值
# 4. 重新写入 Fragment
# 5. 创建新 Manifest

条件更新

python 复制代码
# 更新满足条件的所有行
dataset.update_where(
    filter="status = 'active' AND score < 50",
    updates={"status": "inactive"}
)

# 性能考虑:
# 需要扫描所有 Fragment,找出匹配行
# 对大数据集可能比较慢
# 建议:
# - 使用索引加速过滤
# - 批量更新小部分数据
# - 定期重新组织数据(VACUUM)

📚 总结

Lance 的更新和 Schema 演化特性:

  1. 零成本列添加:只修改元数据
  2. 灵活的类型转换:廉价转换自动优化
  3. 回填机制:支持复杂的数据变换
  4. 向后兼容:时间旅行访问旧 Schema
  5. 原子性更新:所有变更都是事务性的

下一章开始讨论索引系统。

相关推荐
龙山云仓6 小时前
MES系统超融合架构
大数据·数据库·人工智能·sql·机器学习·架构·全文检索
未来龙皇小蓝6 小时前
RBAC前端架构-02:集成Vue Router、Vuex和Axios实现基本认证实现
前端·vue.js·架构
Tadas-Gao6 小时前
深度学习与机器学习的知识路径:从必要基石到独立范式
人工智能·深度学习·机器学习·架构·大模型·llm
啊森要自信7 小时前
CANN ops-cv:揭秘视觉算子的硬件感知优化与内存高效利用设计精髓
人工智能·深度学习·架构·transformer·cann
国强_dev7 小时前
轻量级实时数仓架构选型指南
架构
roman_日积跬步-终至千里7 小时前
【系统架构设计-综合题】计算机系统基础(1)
架构
C澒7 小时前
多场景多角色前端架构方案:基于页面协议化与模块标准化的通用能力沉淀
前端·架构·系统架构·前端框架
代码游侠7 小时前
复习——Linux设备驱动开发笔记
linux·arm开发·驱动开发·笔记·嵌入式硬件·架构
yunteng52117 小时前
通用架构(同城双活)(单点接入)
架构·同城双活·单点接入
麦聪聊数据17 小时前
Web 原生架构如何重塑企业级数据库协作流?
数据库·sql·低代码·架构