最靠谱方案是使用 Apache Arrow C++ 库的 parquet::ParquetFileReader,它自动校验 magic number、解析 Thrift 元数据、支持多压缩算法及谓词下推,避免手动解析的复杂性与错误。

用 Apache Arrow C++ 库读 Parquet 文件最靠谱
直接手写 Parquet 解析器不现实——格式涉及页压缩、字典编码、列元数据嵌套、页头校验、thrift 元数据解析等,C++ 生态里唯一成熟、被广泛验证的选择是 apache-arrow 的 C++ 实现。它封装了全部底层细节,提供 parquet::ParquetFileReader 和 Arrow 表抽象,是当前事实标准。
常见错误是试图用旧版 parquet-cpp(已归档)或自己解析 thrift schema buffer,结果卡在 Invalid parquet file: missing magic number 或解不出重复/定义层级。Arrow 的 ParquetFileReader::Open() 会自动校验 magic bytes 并加载元数据,省掉所有手动跳过 footer、反序列化 FileMetaData 的麻烦。
实操建议:
- 用 v14.0.2+ 版本(支持 INT96 时间戳、LZ4_FRAME、delta encoding 等新特性)
- 链接时确保同时带
-larrow -lparquet -lzstd -llz4 -lbrotli,缺任意一个压缩库都可能触发IOError: Unsupported compression: LZ4 - 若读加密 Parquet(如 KMS 封装),需额外启用
ARROW_WITH_OPENTELEMETRY=OFF并自定义fs::FileSystem,默认不支持
读取时控制列裁剪与类型映射要靠 parquet::ArrowReaderProperties
Parquet 是列存,但默认 FileReader::ReadTable() 会把所有列转成 Arrow Table,浪费内存和 CPU。真正高效的用法是显式指定需要的列名,并控制 Arrow 类型映射逻辑。
立即学习“C++免费学习笔记(深入)”;
例如原始 Parquet 中某列为 INT32 + PLAIN_DICTIONARY 编码,Arrow 默认映射为 dictionary<values: int32 indices:></values:>;但如果你后续要传给 Eigen 或做数值计算,更想要展开后的 int32 数组,就得关掉字典自动提升:
auto props = parquet::ArrowReaderProperties::Defaults();
props.set_use_threads(true); // 启用多线程解码
props.set_pre_buffer(true); // 预分配解码 buffer,减少 realloc
props.set_read_dictionary({{"user_id", false}}); // 强制 user_id 不走 dictionary 展开
其他关键参数:
-
set_batch_size(65536):影响每次从页读取的 record batch 大小,太小导致频繁虚函数调用,太大易 OOM -
set_use_threads(false):某些嵌入式场景需禁用线程池,否则std::thread构造失败会 crash -
set_coerce_int96_timestamp_unit(TimeUnit::MILLI):避免 INT96 时间戳转成纳秒级TimestampArray后溢出
parquet::Scanner 比 FileReader 更适合大文件过滤下推
当 Parquet 文件超过 1GB,且你只想要满足 WHERE ts > '2023-01-01' AND status = 1 的行时,用 FileReader::ReadTable() 先全量加载再 filter 是灾难性的。Arrow 15.0+ 引入的 parquet::Scanner 支持谓词下推(predicate pushdown),能跳过不满足条件的 RowGroup 和页。
核心步骤是构造 Expression 并传入 ScanOptions:
auto expr = and_(greater_than(field_ref("ts"), scalar(timestamp_ms(1672531200000))),
equal(field_ref("status"), scalar(int32_t(1))));
auto scan_options = std::make_shared<parquet::ScanOptions>();
scan_options->use_threads = true;
scan_options->batch_size = 8192;
auto scanner = parquet::Scanner::Make(reader, scan_options).ValueOrDie();
scanner->UseAsync(true); // 启用异步 IO(需编译时开启 ARROW_WITH_THREADS)
auto table = scanner->ToTable(expr).ValueOrDie(); // 谓词在此处下推
注意点:
- 仅支持
EQ/GT/LT/AND/OR等基础表达式,不支持LIKE或正则 - RowGroup 级过滤依赖 Parquet 文件的统计信息(
min/max),如果写入时没写统计(如 Spark 3.0+ 默认关了parquet.enable.dictionary),下推会失效 -
UseAsync(true)在 Windows 上需链接ws2_32.lib,否则运行时报undefined symbol: WSAGetLastError
写 Parquet 时别忽略 WriterProperties 对压缩与页大小的实际影响
很多人用 parquet::ParquetWriter::Open() 写完发现文件比预期大 3 倍,或者 Spark 读报 Unsupported encoding: DELTA_BINARY_PACKED,问题几乎都出在 WriterProperties 配置上。
默认配置对通用性做了妥协:页大小 1MB、压缩用 SNAPPY、不启用字典、不写统计。生产环境应按数据特征调优:
- 高基数字符串列(如 UUID):设
enable_dictionary = true+dictionary_pagesize_limit = 1024 * 1024,能压到 1/5 体积 - 时间序列整数:开
enable_statistics = true+data_page_version = ParquetDataPageVersion::V2,V2 支持 DELTA 编码,比 PLAIN 快 2–3 倍 - 写吞吐敏感场景:增大
data_pagesize = 4 * 1024 * 1024减少页头开销,但会提高延迟 - 跨引擎兼容:禁用
compression = Compression::UNCOMPRESSED或强制compression = Compression::SNAPPY,避免 ZSTD 导致 Trino 报错
最后提醒:Arrow 的 Table 写 Parquet 时,timestamp(us) 列默认按 INT96 存,而 Spark 3.4 默认读 INT64;要么写时加 use_deprecated_int96_timestamps = false,要么读时统一用 Arrow 的 timestamp_us() 类型声明。



















