查询的性能通常不取决于数据处理的速度,而更多取决于能够完全避免处理多少数据。网络数据传输是解耦存储架构中的主要瓶颈。从对象存储读取的每个字节都会产生延迟和费用。因此,最有效的优化方法是确保查询引擎只获取包含所需行的特定文件。两种主要机制实现了这种数据量的减少:分区修剪和文件跳过。尽管它们都旨在减少I/O操作,但它们在查询规划过程中作用的粒度和阶段有所不同。分区修剪分区修剪是首要的防御措施。它在目录级别(或元数据存储中的逻辑分区级别)起作用。当您按照特定列(如date或region)定义分区表时,物理数据会被组织成层级目录。当用户提交一个带有WHERE子句、对分区列进行过滤的查询时,查询规划器会检查目录结构或目录中的分区列表。它会立即抛弃任何不符合过滤条件的路径。此操作发生在规划阶段,通常在存储层上列出或访问任何数据文件之前。设想一个按年和月分区的数据集。如果查询请求2023年1月的数据,引擎会有效忽略所有其他年份和月份。digraph G { rankdir=TB; node [fontname="Helvetica,Arial,sans-serif", style=filled, color="#adb5bd"]; edge [color="#868e96"]; subgraph cluster_0 { label = ""; color = white; root [label="s3://data/sales/", shape=folder, fillcolor="#e9ecef"]; y2022 [label="year=2022", shape=folder, fillcolor="#dee2e6", fontcolor="#868e96"]; y2023 [label="year=2023", shape=folder, fillcolor="#a5d8ff", penwidth=2, color="#339af0"]; y2024 [label="year=2024", shape=folder, fillcolor="#dee2e6", fontcolor="#868e96"]; m01 [label="month=01", shape=folder, fillcolor="#a5d8ff", penwidth=2, color="#339af0"]; m02 [label="month=02", shape=folder, fillcolor="#dee2e6", fontcolor="#868e96"]; f1 [label="data.parquet", shape=note, fillcolor="#4dabf7", fontcolor="white"]; root -> y2022 [style=dashed]; root -> y2023 [color="#228be6", penwidth=2]; root -> y2024 [style=dashed]; y2023 -> m01 [color="#228be6", penwidth=2]; y2023 -> m02 [style=dashed]; m01 -> f1 [color="#228be6", penwidth=2]; } }查询引擎分离出目标路径year=2023/month=01,完全绕过不相关的目录,从而减少元数据开销和扫描。在这种情况下,引擎不需要列出year=2022中的文件。这减少了对对象存储的API调用次数(例如S3的LIST请求),并避免引擎为不相关的数据调度任务。文件跳过与统计信息分区修剪是粗粒度的。它适用于高层次的过滤,但当查询基于非分区列进行过滤时则无效。比如,如果您在2023年1月分区内查询customer_id = 105,分区修剪会将范围缩小到该月份,但引擎可能仍需扫描该文件夹内的数百个文件。这就是文件跳过(也称数据跳过)作用所在。此技术采用存储在文件页脚(Parquet或ORC格式中)或表清单(Iceberg或Delta Lake格式中)的元数据统计信息。用于跳过的最常见统计信息包括:最小值: 文件中(或行组中)某列的最小数值。最大值: 文件中某列的最大数值。空值计数: 该列是否包含空值。跳过如何运作当查询引擎评估一个文件时,它会查看WHERE子句并将其与文件的元数据进行比较。它进行逻辑测试,以判断该文件是否可能包含所需数据。对于过滤条件为WHERE price > 100的查询:文件A (最小: 10, 最大: 50): 范围 [10, 50] 与 > 100 没有重叠。跳过。文件B (最小: 40, 最大: 120): 范围 [40, 120] 与 > 100 有重叠。读取。文件C (最小: 150, 最大: 200): 范围 [150, 200] 完全包含。读取。与下载和解压文件的成本相比,引擎执行此检查所需的CPU资源可以忽略不计。{ "layout": { "title": "基于列范围的文件跳过逻辑", "xaxis": { "title": "列值范围", "showgrid": true, "zeroline": false }, "yaxis": { "showticklabels": false, "title": "文件" }, "shapes": [ { "type": "line", "x0": 65, "x1": 65, "y0": 0, "y1": 1, "xref": "x", "yref": "paper", "line": { "color": "#fa5252", "width": 4, "dash": "dot" } } ], "annotations": [ { "x": 65, "y": 1, "xref": "x", "yref": "paper", "text": "查询: 值 = 65", "showarrow": true, "arrowhead": 2, "ax": 0, "ay": -30, "font": {"color": "#fa5252"} } ], "margin": {"t": 40, "l": 50, "r": 20, "b": 40}, "height": 300 }, "data": [ { "type": "bar", "y": ["文件 1", "文件 2", "文件 3"], "x": [20, 30, 20], "base": [10, 40, 80], "orientation": "h", "marker": { "color": ["#dee2e6", "#339af0", "#dee2e6"] }, "text": ["范围: 10-30 (跳过)", "范围: 40-70 (扫描)", "范围: 80-100 (跳过)"], "textposition": "auto", "hoverinfo": "text" } ] }文件范围的可视化。红线代表一个查询值。只有范围(蓝色条)与查询值相交的文件才会被扫描;其他文件(灰色条)则被跳过。数据布局的作用文件跳过完全取决于数据分布。如果您的数据随机分布在各个文件中,那么每个文件的最小值/最大值范围将有效覆盖整个值域。举例来说,如果客户ID从1到1,000,000随机分散,每个文件可能都有接近1的最小值和接近1,000,000的最大值。这种情况下,没有文件可以被跳过,因为每个文件都可能包含请求的ID。为了最大限度提高文件跳过的效率,数据工程师必须在数据摄取时对数据进行聚类或排序。排序: 通过在写入前对经常查询的列(如customer_id)进行数据排序,您可以确保文件1包含ID 1-1000,文件2包含ID 1001-2000,以此类推。这会创建出明确的、不重叠的范围。Z-排序: 对于基于多列(例如customer_id和transaction_date)的跳过,简单的线性排序是不够的。Z-排序(或希尔伯特曲线)是一种现代格式(如Delta Lake和Iceberg)使用的技术,用于在多维空间中将相关信息放在一起,从而实现多属性上的高效跳过。开放表格式中的跳过传统数据湖方法要求查询引擎打开每个Parquet文件的页脚以读取这些统计信息。尽管这比读取整个文件要好,但仍然需要向对象存储发出大量的GET请求,仅仅是为了读取文件头。现代开放表格式(OTFs)如Apache Iceberg通过将统计信息提升到**清单(Manifest)**级别来改进这一点。清单是一个元数据文件,它列出了数据文件及其上下限。使用OTF时,查询引擎首先读取清单文件。它纯粹在内存中执行过滤逻辑,基于元数据。它生成一个目标文件列表,然后才与对象存储通信以获取实际数据。这种分离使规划阶段与存储层解耦,显著降低了大型表的延迟。效率计算我们可以采用简化的成本模型来估算修剪和跳过的效果。令 $N$ 为总数据量,$S$ 为引擎的扫描吞吐量。不进行修剪时,查询时间 $T$ 大致与数据集大小成线性关系: $$T \approx \frac{N}{S}$$通过分区修剪(将数据量过滤至10%)和文件跳过(根据统计信息跳过剩余文件的80%),有效扫描的数据量 $N_{effective}$ 变为: $$N_{effective} = N \times 0.10 \times 0.20 = 0.02N$$查询现在运行速度提高了50倍,不是因为引擎本身更快,而是因为它处理的数据减少了98%。这种减少直接带来了更低的云成本和更快的分析结果。