一旦数据摄取管道将Parquet或Avro文件存入对象存储,将这些数据资产注册到元数据存储中就变得非常必要。若没有此注册,查询引擎会把数据湖看作一堆文件,而非结构化数据表。本节将为一个销售数据集手动配置数据目录条目,把逻辑表定义映射到物理对象存储路径。此过程建立了必要的抽象层,让Trino、Spark SQL或Amazon Athena等SQL引擎能通过分区裁剪来优化查询。定义物理布局在与数据目录交互前,有必要了解数据在文件系统上的物理组织方式。我们假设电商订单数据集存在于一个S3兼容的存储桶中。数据采用Hive风格的分区策略,即目录名称包含分区列及其值。结构如下所示:digraph G { rankdir=TB; node [shape=box, style="filled,rounded", fontname="Arial", margin=0.2]; root [label="s3://bucket/silver/orders/", fillcolor="#a5d8ff", color="#1c7ed6", fontcolor="black"]; p1 [label="dt=2023-10-25", fillcolor="#63e6be", color="#0ca678", fontcolor="black"]; p2 [label="dt=2023-10-26", fillcolor="#63e6be", color="#0ca678", fontcolor="black"]; f1 [label="part-0001.snappy.parquet", fillcolor="#e9ecef", color="#868e96", fontcolor="black"]; f2 [label="part-0002.snappy.parquet", fillcolor="#e9ecef", color="#868e96", fontcolor="black"]; f3 [label="part-0001.snappy.parquet", fillcolor="#e9ecef", color="#868e96", fontcolor="black"]; root -> p1; root -> p2; p1 -> f1; p1 -> f2; p2 -> f3; }对象存储中分区数据集的层级结构,其中目录代表列值。目录dt=2023-10-25明确告知系统,其中包含的所有文件都属于该特定日期。这种结构对于元数据存储的正确运行是必需的。创建外部表现在,您将使用数据定义语言(DDL)来创建此数据的逻辑表示。在数据湖环境中,我们几乎只使用EXTERNAL表。这种指定方式确保了即使您从数据目录中删除了表定义,存储中的底层Parquet文件也不会被触及。DDL语句执行三种具体的映射:数据结构定义: 列的名称和数据类型。文件格式: 关于如何读取二进制数据(例如Parquet)的指令。位置: 引擎应该查找数据的根URI。在您的查询编辑器(Hive、Spark SQL或Athena)中执行以下SQL命令:CREATE EXTERNAL TABLE silver_orders ( order_id STRING, customer_id STRING, amount DOUBLE, status STRING ) PARTITIONED BY (dt STRING) STORED AS PARQUET LOCATION 's3://bucket/silver/orders/';请注意,dt是在PARTITIONED BY子句中定义的,而不是在主列列表中。这向元数据存储表明,dt是一个从目录结构派生的虚拟列,而非存储在Parquet文件中的物理列。填充分区元数据运行CREATE TABLE语句后,您可能会预期SELECT * FROM silver_orders会返回数据。然而,结果可能为空。发生这种情况是因为元数据存储知道表的根目录(.../silver/orders/),但尚未将特定的子目录(如dt=2023-10-25等)编入目录。查询引擎默认不递归扫描整个存储桶,以避免性能损失。您必须明确指示元数据存储扫描目录结构并注册分区。方法一:手动分区修复对于兼容Hive或Spark的引擎,请使用修复命令:MSCK REPAIR TABLE silver_orders;此命令会扫描表定义中配置的文件系统路径。它会识别遵循key=value模式的文件夹,并将它们添加到分区索引中。方法二:自动化爬网程序 (AWS Glue)在AWS等托管环境中,您通常会使用Glue爬网程序。爬网程序连接到数据存储,确定数据结构,并在数据目录中创建或更新元数据表。当您为路径s3://bucket/silver/orders/配置爬网程序时:爬网程序对文件进行采样以推断数据结构(整型、字符串、双精度浮点型)。它检测分区结构。它用表定义和分区位置填充Glue数据目录。验证数据目录状态一旦分区注册完毕,元数据存储就充当指针系统。当用户运行查询时,引擎会查询数据目录,将逻辑需求解析为物理路径。下图展示了修复后逻辑表定义与物理分区之间的关系:digraph G { rankdir=LR; node [shape=box, style="filled,rounded", fontname="Arial"]; subgraph cluster_metastore { label = "元数据存储 / 数据目录"; style = filled; color = "#f8f9fa"; tbl_def [label="表定义\n数据结构: [order_id, amount...]", fillcolor="#9775fa", color="#7048e8", fontcolor="white"]; part_idx [label="分区索引\n(dt=2023-10-25, ...)", fillcolor="#b197fc", color="#7950f2", fontcolor="white"]; } subgraph cluster_storage { label = "对象存储"; style = filled; color = "#f8f9fa"; s3_path [label="s3://.../dt=2023-10-25/", fillcolor="#ffc9c9", color="#fa5252", fontcolor="black"]; } tbl_def -> part_idx [label="管理"]; part_idx -> s3_path [label="指向", style=dashed]; }元数据存储维护一个分区索引,将逻辑分区值映射到物理存储前缀。您可以通过直接查询分区列表来验证注册情况:SHOW PARTITIONS silver_orders;输出:dt=2023-10-25 dt=2023-10-26查询性能和裁剪数据目录配置完成后,您就可以执行分析查询了。此设置的主要优势是分区裁剪。考虑以下查询:SELECT sum(amount) FROM silver_orders WHERE dt = '2023-10-25';因为数据目录明确地将dt=2023-10-25映射到特定的S3前缀,查询引擎会跳过所有其他目录。如果您的数据集跨越5年,但您只查询其中一天,引擎将读取大约$\frac{1}{1825}$的数据量,从而大幅减少I/O和成本。处理数据结构演变数据湖是动态的。如果您的上游摄取管道在新Parquet文件中添加了一个新列,例如shipping_cost,数据目录不会自动识别它。现有的表定义充当一个严格的筛选器;它会忽略不符合注册数据结构的数据。要使新列可见,您必须更新数据目录定义:ALTER TABLE silver_orders ADD COLUMNS (shipping_cost DOUBLE);此操作仅限于元数据。它更新Hive元数据存储或Glue数据目录中的JSON定义。它不会重写任何物理文件。缺少此列的旧文件在查询该列时将只返回NULL,而新文件将返回相应的值。通过掌握物理数据布局与逻辑数据目录定义的分离,您可以确保数据湖保持可伸缩性和高性能。在下一节中,我们将分析分布式引擎如何运用这些定义来执行向量化查询。