DeepSeek总结的DuckLake 中的数据内联:为数据湖解锁流式处理

news2026/4/9 3:54:14
原文地址https://ducklake.select/2026/04/02/data-inlining-in-ducklake/DuckLake 中的数据内联为数据湖解锁流式处理Pedro Holanda2026-04-02 ·TL;DRDuckLake 的数据内联功能将小批量更新直接存储在目录中从而消除了“小文件问题”使持续流式写入数据湖变得切实可行。我们的基准测试显示与 Iceberg 相比查询速度快 926 倍数据写入速度快 105 倍。数据湖让用户能够避免被锁定在单一数据库中。它们通过将数据以开放格式最常见的是 Parquet存储来实现这一点。大多数数据湖如 Iceberg、Hudi 和 Delta也将其元数据即告诉您某个查询需要读取哪些文件的信息以 JSON 和 Avro 文件等开放格式存储。这意味着任何人都可以实现读取和写入这些格式的系统从而使用户免受单一商业解决方案的锁定。传统数据湖在存储数据时常常伴随着性能问题。问题的根源在于每次小规模写入都会创建一个新的数据文件并更新元数据。这导致存储中充斥着大量的小对象。在读取端查询现在需要遍历越来越多的元数据条目仅仅是为了弄清楚要扫描哪些文件。对于流式工作负载来说这些问题尤其痛苦因为它们会在长时间内执行大量小批量插入每次插入都会创建一个微小的 Parquet 文件和一些元数据文件。每秒一千次插入意味着成千上万这样的小文件不断累积导致性能下降。到了这一步您将被迫进行文件压缩这需要您安排和执行这些维护操作来维持数据湖的运行而维护作业执行时又会对性能造成更大的影响。DuckLake 颠覆了这一模式由于它使用数据库作为其目录它可以将小批量更新直接存储在目录中而不是立即将它们作为 Parquet 文件写入存储。我们将这种技术称为数据内联并将在本文中对其进行描述。示例流式传感器数据流式工作负载的典型例子是以固定间隔更新的传感器数据。作为一个实际例子考虑我们向数据湖插入 100 条观测数据的情况。[点击查看创建传感器数据表的示例脚本]frompyiceberg.catalogimportload_catalogfromdatetimeimportdatetimeimportpyarrowaspa catalogload_catalog(default,**{type:sql,uri:sqlite:///catalog.db,warehouse:file://warehouse,})catalog.create_namespace_if_not_exists(default)schemapa.schema([(sensor_id,pa.int32()),(temperature,pa.float64()),(ts,pa.timestamp(us)),])tablecatalog.create_table(default.readings,schemaschema)foriinrange(100):batchpa.table({sensor_id:pa.array([1],typepa.int32()),temperature:[21.5],ts:[datetime.now()],})table.append(batch)在传统数据湖中每次插入都会创建自己的 Parquet 文件以及相关的元数据文件。运行上面的示例会创建超过 300 个元数据文件200 个 Avro 文件和 101 个 JSON 文件以及 100 个 Parquet 文件。[点击查看 pyiceberg 脚本创建的目录结构]tree warehouse warehouse └── default └── readings ├── data │ ├── 00000-0-01a02dc3-deec-4be5-ab0f-5582e926419a.parquet │ ├── ... └── metadata ├── 00000-0a837115-bff0-4fd7-a3a0-51df4e0b5764.metadata.json ├── ... ├── 01a02dc3-deec-4be5-ab0f-5582e926419a-m0.avro ├── ... ├── snap-1086579672596758615-0-288b7b1d-f159-4692-9404-b2dba114fba2.avro └── ... 5 directories, 401 files如此多的文件使数据层和元数据层都变得臃肿对查询性能和存储账单产生巨大影响。这就是所谓的“小文件问题”。数据湖系统解决这个问题的方法是实施定期的压缩作业将小文件合并成大文件以减少 I/O 成本。但这些压缩例程并没有在写入时解决问题创建所有这些小文件的代价仍然存在并且在它们实际运行之前它们对查询性能没有帮助。DuckLake 采用了一种根本不同的方法。由于目录由用户选择的数据库管理DuckLake 可以将小批量更新、插入和删除直接存储在目录中而不是将它们写为文件。数据库系统几十年来一直专注于高效处理这类小规模读写因此它们天然适合这种工作负载。内联功能还旨在与数据湖的时间旅行特性完全集成。拿上面相同的传感器工作负载在 SQLite 作为目录数据库的情况下针对 DuckLake 运行。[点击查看将数据加载到 DuckLake 的 Python 脚本]importduckdb conduckdb.connect()con.execute(ATTACH ducklake:sqlite:sensors.ducklake AS lake (DATA_PATH sensor_data/))con.execute( CREATE TABLE lake.readings ( sensor_id INTEGER, temperature DOUBLE, ts TIMESTAMP ) )foriinrange(100):con.execute(fINSERT INTO lake.readings VALUES (1, 21.5, now()))# How many Parquet files were created?print(con.execute(SELECT count(*) FROM glob(sensor_data/*.parquet)).fetchone()[0])# 0 -- zero files, everything is inlined in the catalog插入 100 次后我们得到零个 Parquet 文件。所有数据都存在于目录中查询完全按预期工作。下图描绘了在我们的传感器工作负载下传统数据湖与 DuckLake 之间的区别。在 DuckLake 中数据存在于目录中并且在将其刷新到对象存储后只创建一个包含所有数据的 Parquet 文件。[Iceberg 与 DuckLake 在 100 次单行插入后的对比图。Iceberg 创建了 100 个 Parquet 文件和 100 个元数据快照。DuckLake 将所有数据内联存储在目录中产生零个 Parquet 文件并在刷新后将所有内容合并到一个 Parquet 文件中。]在本文的其余部分我们将对 DuckLake 在高竞争流式工作负载下启用和未启用内联功能进行基准测试比较并介绍内联功能在底层是如何工作的。流式基准测试为了了解流式处理在 DuckLake 中的影响我们设计了一个模拟自动驾驶汽车流式传感器数据的基准测试。该基准测试包含一个表其中有 23 个不同类型的列例如ts作为传感器时间戳speed_mps作为表示米/秒的浮点数。插入速率为每秒 100 行分 10 个批次进行每批 10 行。所有插入完成后我们对表列运行 9 个聚合查询例如avg(speed_mps)、stddev(speed_mps)、min(speed_mps)。然后我们执行一个检查点根据系统的不同这会触发压缩、刷新和清理步骤。所有写入操作都由单个duckdb进程执行。我们模拟了 50 分钟的数据包含 300,000 行和 30,000 个批次。目录数据库是 Amazon RDS PostgreSQL 16.10运行在 EC2 c7g.2xlarge 实例上数据存储在同一区域的 S3 存储桶中。步骤无内联有内联性能提升插入1,964 秒375.0 秒5.2 倍聚合查询1,574 秒1.7 秒925.9 倍检查点30 秒2.1 秒14.5 倍使用内联功能后插入速度大约快 5 倍。将数据存储到 PostgreSQL 的往返成本远低于为每个批次将 Parquet 文件写入 S3 的成本。最引人注目的结果是聚合查询性能926 倍的差异。在没有内联的情况下每个查询都必须打开 S3 上所有 30,000 个独立的 Parquet 文件。而在内联情况下数据存在于 PostgreSQL 中查询直接针对它执行完全避免了数千次远程文件读取。对于检查点未内联的情况必须将 30,000 个 Parquet 文件压缩成一个而内联的情况只是将数据从 PostgreSQL 目录刷新到 S3 上的一个 Parquet 文件性能提升了 14.5 倍。我们需要谈谈 Iceberg我们还使用 pyiceberg 和 Apache Polaris 对 Iceberg 运行了基准测试这是在生产环境中管理 Iceberg 表的常见设置。Iceberg 处理 50 分钟的流式工作负载耗时过长因此我们将其缩减到仅 100 秒10,000 行总共 1,000 个批次。步骤Iceberg (Polaris)启用内联的 DuckLake性能提升插入1,148.77 秒10.88 秒105 倍聚合查询83.06 秒0.09 秒923 倍检查点52.83 秒0.28 秒189 倍启用内联的 DuckLake 在所有指标上都快了两个数量级在聚合查询上更是快了近三个数量级。这种差距源于架构Iceberg 在客户端和 PostgreSQL 之间多了一次 REST 跳转并且其快照模型为每个批次写入大约四个 S3 文件而启用内联的 DuckLake 写入零个。这解释了巨大的性能差异。我们尽力为所有系统创建了现实的设置。通过架构和设计更改例如在客户端缓冲写入或插入更大的批次来缓解小文件问题是可能的但这会牺牲 ACID 保证并限制多用户支持这违背了流式写入数据湖的大部分初衷。DuckLake 易于设置欢迎您尝试一下运行自己的工作负载亲自体验其中的差异。内联的工作原理如果您好奇底层发生了什么本节将介绍其内部原理。当您插入、删除或更新的行数低于内联阈值默认为 10时DuckLake 会将更改存储在目录数据库中而不是写入 Parquet 文件。该阈值可以在全局、模式或表级别进行更改-- 全局更改所有表的默认值SETducklake_default_data_inlining_row_limit50;-- 按表为特定表覆盖设置ALTERTABLElake.readingsSET(data_inlining_row_limit100);-- 完全禁用内联SETducklake_default_data_inlining_row_limit0;在实践中这意味着您可以放心地将数据流式传输到 DuckLake而无需担心小文件的激增。DuckLake 通过目录中的插入表和删除表来管理内联数据这些表由规范中的内部表跟踪。在查询时DuckLake 会无缝地将内联数据与任何现有的 Parquet 文件结合起来因此无论数据存在于何处查询总是能返回正确的结果。下面我们介绍每个操作的工作原理。插入当插入的数据量低于内联阈值时DuckLake 不会创建 Parquet 文件。相反它会将行直接存储在目录数据库中的一个内联数据表中。该表包含原始列加上三个元数据列row_id– 该行的标识符begin_snapshot– 插入该行的快照end_snapshot– 删除该行的快照如果仍然存在则为 NULL快照列让 DuckLake 即使对内联数据也能保持完整的时间旅行支持。让我们来看一个具体的例子。首先我们设置一个 DuckLake 目录并创建一个表ATTACHducklake:sensors.ducklakeASlake(DATA_PATHsensor_data/);CREATETABLElake.readings(sensor_idINTEGER,temperatureDOUBLE,tsTIMESTAMP);现在我们插入几个小批次每个批次都低于默认的 10 行阈值INSERTINTOlake.readingsVALUES(1,21.5,2025-03-27 10:00:00);INSERTINTOlake.readingsVALUES(2,22.1,2025-03-27 10:00:10);INSERTINTOlake.readingsVALUES(1,21.8,2025-03-27 10:00:20);这些插入操作都没有创建 Parquet 文件。相反所有三行数据都存在于目录数据库中的一个名为ducklake_inlined_data_table-id_schema-version的内联数据表中。如果我们窥视目录内部ATTACHsensors.ducklakeAScatalog_db;SELECT*FROMcatalog_db.ducklake_inlined_data_1_1;┌────────┬────────────────┬──────────────┬───────────┬─────────────┬─────────────────────┐ │ row_id │ begin_snapshot │ end_snapshot │ sensor_id │ temperature │ ts │ │ int64 │ int64 │ int64 │ int32 │ double │ timestamp │ ├────────┼────────────────┼──────────────┼───────────┼─────────────┼─────────────────────┤ │ 0 │ 2 │ NULL │ 1 │ 21.5 │ 2025-03-27 10:00:00 │ │ 1 │ 3 │ NULL │ 2 │ 22.1 │ 2025-03-27 10:00:10 │ │ 2 │ 4 │ NULL │ 1 │ 21.8 │ 2025-03-27 10:00:20 │ └────────┴────────────────┴──────────────┴───────────┴─────────────┴─────────────────────┘每次插入都创建了一个新的快照但没有创建新文件。所有行的end_snapshot都是 NULL因为还没有行被删除。注意begin_snapshot从 2 开始因为CREATE TABLE语句本身占用了快照 1。如果删除操作的目标是仍然内联的行DuckLake 会通过设置该行的end_snapshot列来就地处理。不会创建删除文件。例如DELETEFROMlake.readingsWHEREsensor_id2;┌────────┬────────────────┬──────────────┬───────────┬─────────────┬─────────────────────┐ │ row_id │ begin_snapshot │ end_snapshot │ sensor_id │ temperature │ ts │ │ int64 │ int64 │ int64 │ int32 │ double │ timestamp │ ├────────┼────────────────┼──────────────┼───────────┼─────────────┼─────────────────────┤ │ 0 │ 2 │ NULL │ 1 │ 21.5 │ 2025-03-27 10:00:00 │ │ 1 │ 3 │ 5 │ 2 │ 22.1 │ 2025-03-27 10:00:10 │ │ 2 │ 4 │ NULL │ 1 │ 21.8 │ 2025-03-27 10:00:20 │ └────────┴────────────────┴──────────────┴───────────┴─────────────┴─────────────────────┘传感器 2 的行现在有了end_snapshot 5意味着它在快照 5 中被删除了。常规查询会将其过滤掉但时间旅行查询仍然可以看到它。删除删除内联处理的是另一种情况删除已经存在于 Parquet 文件中的行。DuckLake 不会重写 Parquet 文件或创建单独的删除文件而是在目录中的一个按表划分的内联删除表中记录删除操作。该表跟踪哪些 Parquet 文件中的哪些行已被删除以及导致删除的快照。例如假设我们有一个data.parquet文件其中包含一些我们想添加到表中的传感器读数CALLducklake_add_data_files(lake,readings,data.parquet);SELECT*FROMlake.readings;┌───────────┬─────────────┬─────────────────────┐ │ sensor_id │ temperature │ ts │ │ int32 │ double │ timestamp │ ├───────────┼─────────────┼─────────────────────┤ │ 1 │ 20.0 │ 2025-03-27 09:00:00 │ │ 2 │ 19.5 │ 2025-03-27 09:00:10 │ │ 3 │ 21.2 │ 2025-03-27 09:00:20 │ │ 4 │ 18.8 │ 2025-03-27 09:00:30 │ └───────────┴─────────────┴─────────────────────┘现在如果我们从这个文件中删除一行DuckLake 不会重写 Parquet 文件。相反它会在目录中创建一个名为ducklake_inlined_delete_table-id的内联删除表DELETEFROMlake.readingsWHEREsensor_id3;SELECT*FROMcatalog_db.ducklake_inlined_delete_1;┌─────────┬────────┬────────────────┐ │ file_id │ row_id │ begin_snapshot │ │ int64 │ int64 │ int64 │ ├─────────┼────────┼────────────────┤ │ 0 │ 2 │ 6 │ └─────────┴────────┴────────────────┘这个条目告诉 DuckLake文件 0 中的第 2 行在快照 6 中被删除了。在查询时DuckLake 会从 Parquet 文件扫描中过滤掉这一行因此它永远不会出现在结果中。更新更新操作就是一次删除后跟一次插入因此它们遵循上述完全相同的步骤并得到完全支持。刷新内联数据当然会随着时间的推移而增长因此 DuckLake 还提供了一个刷新操作将内联的行物化为合并后的 Parquet 文件。这在性能有要求或出于迁移目的时非常有用。-- 刷新目录中的所有内联数据CALLducklake_flush_inlined_data(lake);-- 仅刷新特定表CALLducklake_flush_inlined_data(lake,table_namereadings);或者刷新也是检查点例程的一部分该例程按顺序运行所有维护操作刷新、快照过期、文件合并和清理CHECKPOINTlake;结论“小文件问题”一直是数据湖处理流式工作负载的主要痛点之一。对于此类工作负载传统数据湖格式在写入时会产生小文件然后在后续的维护作业中进行清理。DuckLake 通过将小规模更改直接存储在目录中来完全避免这个问题而数据库系统几十年来一直在优化这种类型的工作负载。传统数据湖启用内联的 DuckLake小批量插入创建一个 Parquet 文件存储在目录中小批量删除创建一个删除文件存储在目录中1,000 次插入后的文件数1,000 个文件0 个文件需要压缩是定期进行否准备好时刷新即可查询性能随文件数量增加而下降不受小批量写入影响配置需要调优开箱即用内联功能开箱即用无需任何配置。插入、删除和更新都得到支持。当数据准备好存储为 Parquet 文件时只需一个检查点即可完成。数据内联功能将随 4 月份发布的 DuckLake 1.0 和 DuckDB v1.5.2 一起提供但您不必等待。您可以从 DuckDB v1.5.1 的core_nightly仓库安装 DuckLake。FORCEINSTALL ducklakeFROMcore_nightly;LOADducklake;然后您可以将其指向一个流式工作负载亲自体验其中的差异。运行您自己的基准测试感受它的魅力。设置只需五分钟运行无需任何配置。如果您遇到任何问题请在 GitHub 上提交 issue。如果您遇到特殊情况并希望进行讨论我们在 DuckDB Discord 频道上有一个活跃的社区。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2480896.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…