Parquet 查询为什么仍然很慢:分区、行组统计与裁剪验收

把 CSV 转成 Parquet 后,查询仍可能读取大量无关数据。关键不是扩展名,而是查询条件能否对应分区和行组统计、数据排列是否形成可排除的范围,以及读取端是否真正使用这些信息。本文用可重算的小型订单示例解释裁剪机制,比较两种布局,给出元数据、执行证据和结果一致性的验收流程,并处理边界值、缺统计、小文件与版本混读等失败情况。

Parquet 查询慢时,先证明哪些文件、列和行组可以被排除,再判断执行端是否真的少读了数据。返回十行不代表只扫描十行,执行计划出现过滤条件也不等于减少了远端传输。本文面向数据工程与企业分析团队,以 DuckDB 2.0 文档和 Apache Parquet 格式说明为参考,提供未执行的教学示例与验收方法;数值是可手工复核的构造数据,不是性能实测或通用调优阈值。

先固定问题:结果少,不代表扫描少

设想一份订单明细只查询一天中某家工厂的已完成记录,结果很少,耗时却随历史文件总量增长。需要先区分时间花在文件发现、元数据读取、数据传输、解压、过滤、聚合还是结果输出。扩容可能暂时缩短某一环节,却不能解释为什么不相关年份的文件也进入读取路径。工程目标应同时包含结果正确与资源减少,而不是只要求一个更短的总耗时。

Apache Parquet 将一组行组织为行组,每个行组中按列形成列块,列块再包含数据页;文件尾部元数据描述列块位置。[1] 因此只需要少数列时,读取端有机会避免读取其他列;知道某个行组不可能满足条件时,又有机会跳过该组。文件格式提供结构,具体优化仍取决于写入信息、查询形式与读取实现。

本例约定 order_id 为唯一整数,order_day 为已明确业务口径的 DATE,plant 为工厂代码,qty 为非负整数,status 为状态枚举。一天属于哪个时区和业务截止规则须在入湖前确定;本文不把日期字符串截断当成通用转换。输出为指定日期、工厂及状态下的订单数和数量和。所有候选布局必须使用同一份冻结记录,才能把差异归因于布局。

实施前记录读写引擎版本、插件或扩展版本、文件清单及对象版本、压缩方式、行组行数、线程数、内存限制与存储位置。本文依据的是阅读当日 DuckDB 2.0 文档,不能用它推断旧版本所有行为一致。尤其要把预期、执行计划和实际读取证据分开:文档说明支持优化,并不代表当前文件和查询已触发。

分区、列裁剪、行组裁剪分别解决什么

分区裁剪利用目录键或受信任的文件清单排除整个文件。例如按 order_day 分目录,查询十一月二日时,读取端可根据相应分区条件排除其他日期。DuckDB 的 Hive 分区文档说明了从目录读取分区值及下推分区过滤的行为。[2] 但文件发现仍可能有成本,路径中写着某日期也不证明文件内容确实属于该日,写入时必须核对。

列裁剪只保留计算需要的列。即便最终输出只有数量和,过滤条件中的 plant、status 以及必要的日期列也可能需要读取;order_id 若只做 COUNT(*) 则未必需要。不要把输出列数等同扫描列数。反过来,应用先 SELECT * 拉回所有字段再做过滤,会失去让存储扫描阶段减少列读取的机会。DuckDB 文档明确支持将投影与过滤推入 Parquet 扫描。[3]

行组裁剪依赖可用且可信的统计边界。对闭区间查询 a≤x≤b,若行组最大值小于 a,或最小值大于 b,可排除该组。若范围相交,只说明可能命中,仍需读值并计算完整条件。对半开区间 a≤x<b,则排除条件是最大值小于 a,或最小值大于等于 b。等号放错位置,会把边界记录误删。

多个列的最小最大值一般不是联合分布。某行组包含十一月一日的 A 厂订单和十一月三日的 B 厂订单,日期范围与工厂范围都可能覆盖查询条件,但未必存在十一月二日的 A 厂记录。统计不能证明存在时允许多读,不能因猜测不存在而少读。裁剪的正确目标是无遗漏地排除确定不可能命中的范围,而不是精确预测结果行数。

页索引是另一层可选信息,不能与行组统计混为一谈。Parquet 规范描述 ColumnIndex 与 OffsetIndex 用于定位可跳过的数据页。[4] 是否写出、读取端是否使用,应分别确认。Apache Arrow 的 write_table 文档明确说明 sorting_columns 只是声明排序信息,不负责排序或验证;该文档还提示 PyArrow 读取端尚未使用页索引。[5] 写了参数而没有实际能力证据,不足以宣称优化成功。

构造十二条订单,手工推导两种布局

下面十二条记录构成完整教学输入,日期均为 2026 年十一月,状态均为 completed;字段顺序为 order_id、order_day、plant、qty。记录为:1、11-01、A、10;2、11-01、B、20;3、11-01、A、30;4、11-01、B、40;5、11-02、A、50;6、11-02、B、60;7、11-02、A、70;8、11-02、B、80;9、11-03、A、90;10、11-03、B、100;11、11-03、A、110;12、11-03、B、120。日期在文件中必须保存为完整 DATE,而非这里为便于阅读所写的短日期。

查询目标是十一月二日 A 厂 completed 订单。满足条件的是编号 5 和 7,订单数为 2,数量和为 50+70=120。全表数量和为 780,可作一个辅助基线,但总和一致不能替代主键和逐行一致。应先把这些预期固定,再生成物理文件;如果优化后结果变成 60 或 240,应先查数据而不是表扬延迟降低。

为说明机制,假设每个教学行组恰有四行;这是人工分组,不是建议 DuckDB 写四行行组。布局甲按编号顺序存放:第一组为 1—4,日期全是十一月一日;第二组为 5—8,日期全是十一月二日;第三组为 9—12,日期全是十一月三日。三组的日期统计可排除第一、第三组,候选行数为四,随后按工厂筛出两行。

布局乙含完全相同记录,却交错存放:第一组为 1、5、9、2;第二组为 3、6、10、7;第三组为 4、8、11、12。每组日期最小值都为十一月一日,最大值都为十一月三日,也都同时含 A 和 B。因此依据这些统计,三组都不能排除,候选行数为十二;最终结果仍必须是两行、数量和一百二十。

甲相对乙的候选行组从三组降到一组,候选行数从十二降到四,但不能推出实际字节减少三分之二,更不能推出耗时降低三分之二。列块压缩大小、页布局、读取合并、缓存和请求开销都会改变真实成本。这组计算证明的是排列改变统计可排除性,不是某软件的性能倍率。

把甲改为按日期分三个目录,则分区条件可以先排除两天文件。目标日文件若只有一个行组,仍要在四条记录中筛工厂。再按工厂拆分能减少候选行,却增加文件数量;对十二条记录显然不值得当作生产布局建议。这个小例子用于完整核对输入到输出,生产选型必须使用具有代表性的记录规模和查询分布。

在真实引擎中核对元数据与扫描证据

第一步,准备两个独立目录保存同一冻结数据的不同排列,使用相同列类型、压缩配置和写入实现。改变排列时必须移动完整记录,不能分别排序各列。将教学行数扩展到足以形成多个真实行组的规模,同时保留可重复生成规则和期望结果。不要对正在服务的目录原位覆盖,以免查询读取一半旧文件、一半新文件。

第二步,检查实际写出的行组,而非假定写入参数就是最终边界。DuckDB 文档提供 parquet_metadata、parquet_schema 等检查入口。[3] 可执行 SELECT * FROM parquet_metadata('orders_sorted.parquet'); 作为诊断示例,核对每个文件的行组编号、行数、目标列统计和压缩字节。具体字段名以部署版本返回为准,本示例未运行,不能报告元数据已经符合预期。

DuckDB 2.0 的 Parquet Tips 将 ROW_GROUP_SIZE 解释为行组最小行数参数,最小值受向量大小约束,文档列出 2048 的下限。[6] 因而本文四行分组不能直接套成其 COPY 参数。PyArrow 的 row_group_size 又有自身语义。[5] 跨写入器时应比较实际文件,而不是拿同名配置假设相同布局;测试说明中必须写清采用哪个写入器。

第三步,在非分区文件上运行完整条件示例:SELECT COUNT(*) AS order_count, SUM(qty) AS total_qty FROM read_parquet('orders_sorted.parquet') WHERE order_day = DATE '2026-11-02' AND plant = 'A' AND status = 'completed'; 对乱序版本运行相同查询。在十二行输入上预期为 2 和 120;扩大输入后按生成规则重新计算预期,不能沿用小样本答案。

第四步,用 EXPLAIN 检查扫描中的投影和过滤,再用部署版本支持的分析或性能记录观察实际执行。若只有输出行数或算子耗时,没有读取字节或行组跳过计数,就只报告已见证据。可以结合存储侧请求日志、传输字节和文件元数据估算候选范围,但必须标注估算,不把可能跳过的行组数冒充引擎真实跳过数。

第五步,分别测量首次访问与重复访问,说明缓存是否受控,不把热缓存结果包装成冷读取。固定硬件、线程、存储和查询输出消费方式,交替运行不同布局并保留多次结果,而非只挑最快一次。统计总耗时之外,还要报告文件数、元数据体积、扫描字节、CPU、峰值内存和结果一致性;拿不到的指标明确留空。

按日期分区还是只排序:用查询组合选择

方案一是在少量较大文件内按常用过滤键排列,例如先日期再工厂,保留适量行组。它减少目录与小文件管理负担,适合主要按时间范围查询、写入可分批整理的场景。代价是跨批次追加可能重新拉宽统计范围,而且次级排序只在主键相同的范围内有效;按日期优先排序不保证跨全年查单个工厂同样高效。

方案二是按低至中等基数的日期粒度分区,再在分区内排序与合并文件。按日还是按月,要看每天数据量、典型时间跨度和重写频率。它便于按范围排除文件、替换某个历史分区,但会增加目录枚举、文件清单与迟到数据维护成本。日期分区键必须来自约定业务时间,不能因为路径易生成就改用上传日期。

不要对高基数订单编号逐值建目录。即使一次查询只命中很少文件,海量小文件的发现、权限检查与元数据读取也可能成为主要开销。合并文件时又不能一味追求最大文件:行组过大可能降低细粒度裁剪和可用并行度,过小则增加统计与调度成本。[6] 选择应围绕实际混合查询,不能把某个默认行数当成所有数据集的最佳值。

建议固定至少三类工作负载:窄日期加工厂过滤、跨月聚合,以及按订单号追查。为每类记录频率、业务时限、扫描成本和维护代价。若日期布局明显改善日报却使关键追查不可接受,可以保留专用索引或另建服务路径,而非把所有查询强塞进单一文件布局。Parquet 不是要求企业放弃数据库索引的理由。

比较总成本时将排序、压缩、合并、历史重写和存储增量列入。可用每周期查询节约资源减去额外写入与维护资源形成内部账目,单位要保持一致;CPU 秒不能直接与货币相减。若查询很少、数据很小,维持简单布局可能更合理。优化的意义是降低完整工作负载成本,而不是把某一个演示查询做到最快。

五组失败用例:每组都核对结果与修正

用例一是边界日期。输入包含范围起点、终点前一刻和恰好终点,查询采用起点包含、终点不包含的时间范围。预期只包含前两类;检查返回主键与类型转换后的时刻,而非只看行数。若把终点等号写错,修正过滤和分区规则,再对跨日与跨月边界复验。涉及时间戳时还需确认时区语义,本例 DATE 口径不能替它作保证。

用例二是没有可用统计或统计范围很宽。对同一数据分别关闭统计写入、打乱排列,预期结果完全一致,但不能再依据缺失边界宣称可排除。检查文件元数据与实际扫描证据;修正可选择重新写出统计、调整排列,或接受该查询需要全扫描。复验时同时跑命中与不命中条件,确保优化没有让合法记录消失。

用例三是空值和未知分区。新增日期为空的订单,并分别查询日期相等与日期为空;预期前者不包含该行,后者能按约定找到它。不能把缺失最小值解释成最早日期,也不能把未识别目录悄悄忽略。检查空值计数、分区清单和结果主键;修正为明确的未知值处理流程,再核对全量总数与分组总数能否在既定口径下对齐。

用例四是查询表达式阻碍优化。将直接日期条件换成复杂格式化函数或自定义函数后,结果可能一样,但过滤不一定能被推入扫描。预期是语义不变,裁剪能力需要重新观察。检查执行计划的过滤位置和传输指标;若无法下推,考虑预先保存规范字段或使用等价且可被引擎识别的条件。修正后先比结果,再比扫描,不可为性能改变业务口径。

用例五是分区路径与内容不一致。把一条十一月二日记录误放到一日目录,依靠目录过滤可能直接排除它。验收应由原始业务日期独立核对所属分区,不能只读取引擎从路径推导出的同名字段作为证据。先隔离错误文件、重建正确分区和清单,再对全量扫描与带分区过滤的结果做双向主键差集;差集为空才证明该测试范围没有漏增。

还应注入缺文件、重复文件和合并中断。一个目录下同时保留旧小文件与新合并文件,查询可能把订单计算两遍,且每个文件都格式正确。预期失败应阻断新版本对外切换,保留旧清单继续服务;检查唯一键集合、重复计数、数量和以及发布版本。修复不应靠查询时盲目 DISTINCT 掩盖重复写入,而应恢复唯一、完整的文件集合。

上线清单:把物理优化与数据发布分开

建立一份可复用验收记录:输入版本与完整文件清单;字段类型、单位和空值规则;分区键来源;实际行组数量与范围;查询文本及预期主键;可见的计划与扫描证据;冷暖运行条件;写入维护成本;失败用例结果;切换与回退负责人。每项保存真实值或明确未测,不用一个优化百分比代替整份证据。

版本切换采用新目录和固定清单,先验证新集合完整,再让消费者切换到同一版本。对象存储与查询引擎是否提供原子清单切换,应按实际系统确认,不能把目录改名视为跨平台通用事务。旧版本留存遵守数据保留与权限规则;回退须确认旧清单仍可访问,且不会把新收到的记录误认为已经包含。

物理布局变化不应顺手改变业务字段。若合并文件同时调整列类型、金额单位或时间定义,应另走数据管道的 Schema 兼容与语义契约验收。性能测试只能证明在指定数据与查询下的行为,无法替代字段含义和消费者兼容性验证。对 AI 问数还要确认它生成的真实查询符合已测试的过滤形式,而不是只测试工程师手写的理想 SQL。

运行后观察小文件数量、每分区文件数、统计范围变宽、常见查询扫描量和合并积压。迟到数据可能使已经整理的分区再度零散,应把修复频率与业务可见时限一起设计;不能为了压缩效率无限延迟数据发布。新增查询如果主要按另一个维度过滤,要重新评估布局,而非默认旧排序永远有效。

可以接受没有改善的实验结果。若数据量不足以摊薄整理成本,或查询多数需要全量读取,就保留简单方案并记录原因。只有在结果一致、扫描证据支持、代表性负载受益且维护代价可承担时,才扩大上线范围。一个可解释的未优化决定,比没有证据的速度承诺更适合作为数据工程交付。

参考资料

  1. [1] Apache Parquet — File Format(2026-11-09 阅读)
  2. [2] DuckDB 2.0 — Hive Partitioning(2026-11-09 阅读)
  3. [3] DuckDB 2.0 — Reading and Writing Parquet Files(2026-11-09 阅读)
  4. [4] Apache Parquet — Page Index(2026-11-09 阅读)
  5. [5] Apache Arrow 26.0.1 — pyarrow.parquet.write_table(2026-11-09 阅读)
  6. [6] DuckDB 2.0 — Parquet Tips(2026-11-09 阅读)
返回洞察
鲁ICP备2024109755号-2
可拖动移动。右键、长按或按 Shift+F10 可选择停靠位置。