pyarrow实战:列式存储、Parquet读写与大数据内存优化 大数据处理这件事很多人一开始接触到的都是 pandas。说实话pandas 在处理几十万、几百万行数据时确实顺手但一旦数据量上了几个 GB画风就变了要么内存占用直接拉满要么读取慢到怀疑机器是不是坏了。我最早遇到 pyarrow其实是在做 Parquet 文件读写时被同事安利的后来深入用下来发现这个库远不止“Parquet 读写工具”这么简单。Apache Arrow 定义了一套跨语言的标准列式内存格式pyarrow 就是它的 Python 接口它非常适合做大吞吐量数据搬运、格式转换和大规模数据分析。这篇文章我会从多个维度去拆解 pyarrow 的实际用法和踩坑经验说清楚它到底解决了什么问题、适合哪些场景、怎么用才能真正吃满硬件性能而不是停留在“能用”这种层面。如果你是数据工程师、数据分析师或者正在做机器学习特征管道只要你的数据量超过了单机内存能轻松装下的水位线pyarrow 都值得你花一天时间把它过一遍。下面我就从白纸到实战把整个过程拆开来讲。1. 为什么大规模数据处理绕不开 Arrow1.1 列式存储的内存布局到底赢在哪里传统上我们用 pandas 处理数据时DataFrame 底层是行式存储逻辑每个列各自有独立的 block其实 pandas 内部也是按列来管理数据的但真正的问题出在数据类型和对象引用上。比如一列字符串pandas 会把它存成 Python 对象数组每个字符串都是一个独立的 Python 对象内存里除了字符串本身还要维护一套指针和对象头信息数据量一大内存翻倍增长不说CPU 在检索时还会频繁发生 cache miss。Arrow 的列式内存格式却不同它是连续的内存块每个列的所有元素在物理上按固定宽度排列在一起字符串类型则采用类似“连续字节区 偏移量数组”的布局。这种布局在读取时能最大程度发挥 CPU 的缓存友好性你可以把 Arrow 的内存比作一盒预先分好类的巧克力每一格只装同一种口味想吃某个口味直接去对应那一格拿不需要在一片混合巧克力里一颗颗翻找。而行式存储就像一盒混合口味巧克力你必须一颗颗掰开看完才知道味道CPU 在这种模式下做一次全列扫描效率差距非常明显。另一个关键点是压缩率。列式存储因为同一列的数据类型相同、数值分布往往有规律可以做到很高的压缩比。Arrow 在内存中常用的字典编码和位压缩让整列数据在高度重复的场景里能压缩到原始体积的十分之一甚至更低。这一点对大规模数据处理来说意味着一份数据能放下更多内容I/O 压力也大幅下降。1.2 零拷贝设计改变了数据交换方式所谓零拷贝不是说数据没有任何复制而是指数据在进程之间、不同语言运行时之间传递时不需要经历“序列化-传输-反序列化”这个传统链路。Arrow 通过标准化的内存格式让数据在生成之后可以直接被另一个支持 Arrow 的组件读取大家共享同一块内存区域谁都不需要重新解析。举个我实际遇到的例子。我曾经有一个 C 模块负责做实时特征计算Python 侧需要消费这些结果。传统方案是 C 算完转成 JSONPython 再用 json.loads 去解析几十万条特征下来光解析就要花几秒。后来我们把数据放到 Arrow 内存格式里C 侧直接写 Arrow ArrayPython 侧用 pyarrow 从同一块内存 buffer 里读出来转成 pandas DataFrame时间直接从秒级降到百毫秒级。这个体验非常直观地解释了零拷贝的意义数据还是那份数据关键是你不用再花大把时间去把内存里的二进制解释成一层层 Python 对象。大数据生态里 Arrow 的定位其实很像一个通用交换中枢。你从 Postgre 导数据到 ClickHouse从 Spark 算完交给 Python 做可视化过程里如果都经过 Arrow速度和内存占用都会得到极大改善。这也是为什么 Flink、Spark、DuckDB 等很多引擎都在核心层面拥抱 Arrow因为它把数据格式和传输协议统一了减少了多系统协作里的重复转换成本。1.3 pyarrow 在生态里的角色pyarrow 不只是一个 Python 库它是 Apache Arrow C 官方实现的 Python 绑定。也就是说你调用 pa.array、pa.table底层跑的是 C 那边的高性能代码而不是 Python 纯循环。这种绑定还有个好处就是类型体系非常严谨。pyarrow 里几乎能表达所有常见的结构化数据形式int8、int16、int64、uint64、float16、float32、float64、decimal128、timestamp、duration、date32、string、binary、list、struct、map、union再往上还有更复杂的 nested 结构。这种严格的类型定义在跨系统交换时特别重要因为数据一旦到 Python 层面很多类型信息就模糊了。比如 pandas 里的 int64 列被塞进了一个 NaNpandas 会悄悄把整列升级成 float64这在数据量大的时候不仅浪费内存还会改变字段语义。Arrow 在做转换时不会这么草率Null 就是 Null类型是什么就是什么来回转换不会丢精度、不会隐式升级类型。2. pyarrow 的快速上手与常用数据类型2.1 安装和环境准备安装 pyarrow 非常简单直接用 pip 就行pyarrow 的 wheel 包是预编译好的不需要你本地装 C 编译器。pip install pyarrow如果是在 conda 环境里我更推荐用 conda 装因为它会自动处理 Arrow 相关 C 扩展库的依赖匹配问题conda install pyarrow建议你尽量使用比较新的版本。pyarrow 的迭代速度很快较早版本在一些 API 上会有变化比如pa.Table.from_pandas的参数、pq.write_to_dataset的弃用提示这些在你装完做二次开发时都会影响到写法。装好之后可以快速验证一下版本号import pyarrow as pa print(pa.__version__)我用过的几个版本里从 8.x 到 15.x 性能都在稳步提升尤其是字符串类型和 nested 类型的内存优化越新的版本效果越明显。2.2 从数组和表开始建立直觉pyarrow 里你接触最多的三个抽象是Array、ChunkedArray和Table。Array就是同一数据类型的列ChunkedArray是由多段 Array 组合成的列Table则是由若干列组成的一个结构化数据集。下面这段代码你应该跑起来看看结果它会给你的数据结构建立一种很直观的感受import pyarrow as pa arr pa.array([1, 2, 3, None, 5]) print(arr) print(arr.type) print(arr.to_pylist()) # 转回 python listNone 保留pyarrow.lib.Int64Array object at ... [ 1, 2, 3, null, 5 ] int64 [1, 2, 3, None, 5]这里有一点很值得注意Arrow 数组天然支持 null 值它与缺失值是同一种概念但在内存里有 mask 标记哪些位置是 null。也就是说你不会因为数组里有缺失值就把整个列的数据类型从 int64 升级成 float64这一点和 pandas 的行为完全不同。再来建一张表table pa.table({ name: [alice, bob, carol], score: [88.5, 92.0, 79.5], age: [23, 27, None], }) print(table.schema)name: string score: double age: int64你看到 schema 清晰地标注了列名和类型这在你和别人协作、或者跨系统传数据时特别有用相当于给数据加了一份说明书。数据的类型一目了然任何下游拿到这个 schema 后都知道怎么解析数据不需要再试探性推断。2.3 与 pandas DataFrame 的双向转换这一节应该是大多数读者最先用到的功能。pyarrow 和 pandas DataFrame 的互转在很多场景下替代了原先手动循环、反复 to_dict 再重新构造 DataFrame 的写法。从 pandas 转到 Arrowimport pandas as pd import pyarrow as pa df pd.DataFrame({ id: range(100_000), value: [x * 2 for x in range(100_000)], }) table pa.Table.from_pandas(df)从 Arrow 转回 pandasdf_again table.to_pandas()看起来很简单但这里有几个值得注意的细节。第一个是当你的 DataFrame 索引不是默认的RangeIndex时to_pandas默认会尝试保留索引会额外付出时间。如果你不需要保留索引可以这样df_again table.to_pandas( preserve_indexFalse )第二个是字符串列。默认情况下Arrow 的string类型转回 pandas 时会变成object类型的 Python 字符串数组这个过程会创建大量 Python 对象如果你紧接着还要做后续计算反而不如直接用 pandas 的StringDtype。所以遇到超大字符串列时建议你在依赖侧做好取舍该用 Arrow 的字符串列操作就用 Arrow 操作不要反复转换成 pandas。还有一个点我实际踩过pa.Table.from_pandas默认会启用preserve_index如果你本来 pandas DataFrame 就有一个无意义的索引列这个索引可能也会被保留成 Arrow 的一列数据量一上来这就会浪费内存。所以我在做 ETL 管道时通常会在从 pandas 转 Arrow 前主动reset_index(dropTrue)或者显式传preserve_indexFalse。2.4 不只是 pandas还有 Protocol Buffers 那种结构化协议可替代性这里多说一句。很多团队在系统间传递结构化数据时第一反应是 JSON 或者 Protocol Buffers。JSON 的可读性好但解析慢、体积大Protocol Buffers 要维护.proto文件写起来更繁琐。Arrow 在内部系统传递场景其实是一个被低估的替代方案。它既有严格的 schema又有极致的内存效率还方便转成 pandas 或 numpy 继续分析。我自己在公司内部搭数据管道时数据服务端如果直接暴露 Arrow IPC 格式接口客户端用 pyarrow 读取后就可以直接做聚合分析根本不需要中间落地成 JSON 文件再清理一遍。这种开发体验和性能是传统交换方式很难比的。3. Parquet 文件读写的正确打开方式3.1 Parquet 与 Arrow 的天然配套Apache Parquet 是一种列式存储文件格式Arrow 则是列式内存格式这两个在思路上完全同频配合使用效果就是 112。数据从 Parquet 读进内存以后Arrow 表可以直接参与计算计算完以后又可以直接写回 Parquet中间几乎不需要额外转换。Parquet 相比 CSV 的核心优势有三点第一文件体积小因为列式结构有更好的压缩空间第二读取速度快因为下游只关心部分列时它可以跳过不相关列的数据块第三自带 schema支持嵌套结构更贴近真实业务中的复杂数据模型。3.2 写 Parquet 时压缩方式和 row group 大小怎么选已知读写 Parquet 最常见的方式是pyarrow.parquet模块import pyarrow as pa import pyarrow.parquet as pq import pandas as pd df pd.DataFrame({ user_id: range(1_000_000), score: [i % 1000 for i in range(1_000_000)], }) table pa.Table.from_pandas(df) pq.write_table( table, example.parquet, compressionzstd, row_group_size100_000, )压缩算法这块我个人的排序是zstd 在压缩比和解压速度之间平衡最好日常首选如果对压缩速度要求极高、CPU 资源紧张就用 snappy如果想把文件压到极小可以选择 gzip但要接受更长的压缩时间。下面是我在几份不同数据上做过的简单对比虽然没有严格基准测试那么严谨但趋势很明显压缩算法压缩率相对大小写入速度读取速度适用场景snappy中等很快很快日常读写均衡zstd更小快快推荐日常默认gzip最小较慢中等数据长期归档brotli很小较慢中等极限压缩不常读取但要注意压缩率不是越高越好。如果数据是一次写入、频繁读取那么读取解压时的 CPU 开销是主要成本zstd 是常见默认gzip 在某些场景解压速度不够好反而会拖慢整体任务。row_group_size这个参数很多人会忽略。row group 是 Parquet 文件里数据读取的并行单位默认值通常在两万行左右。你可以在写入时把它调大比如 10 万行或 50 万行。row group 大一点文件头信息更简单整体压缩率也会更好但如果后续读取时只查一小片数据粒度太粗反而不利于谓词下推。实际操作时我建议普通分析场景用 10 万行如果下游多次读取不同分区可以适当调小。3.3 读 Parquet 时的列裁剪和谓词下推这一个是 Parquet 效率的灵魂所在。当你只需要表里的两列时千万别急着把全表 load 进来再取列而是直接在读取时就传columns参数让 Arrow 跳过其他数据块import pyarrow.parquet as pq table pq.read_table( example.parquet, columns[user_id, score], )假如你想过滤score 500的用户而且文件里本身按某个字段做了排序或者有统计信息Arrow 还能做谓词下推在读取阶段就跳过不可能命中的数据块table pq.read_table( example.parquet, filters[(score, , 500)], )这里的filters支持一个 list of filters多个条件之间默认按 AND 处理。要注意的是谓词下推效果受数据布局影响很大。如果某个列的数值分布在整个文件里均匀打散过滤时仍然可能需要扫描大量 row group如果数据本身按照过滤字段做了排序或分桶那读取速度会有质的提升。所以在大规模数据管道里写入时按常用过滤字段排序比事后加索引有时候更实用。3.4 分片数据集和目录分区的处理方式当数据量达到 TB 级别单文件 Parquet 就无法满足了。常规做法是把数据按照某个字段分区存储在目录结构里比如按日期分区data/ dt2024-01-01/ part-0.parquet part-1.parquet dt2024-01-02/ part-0.parquetpyarrow 提供了一个更上层的DatasetAPI可以像查询一个逻辑表一样操作整个目录import pyarrow.dataset as ds dataset ds.dataset(data/, formatparquet, partitioninghive) table dataset.to_table(filterds.field(dt) 2024-01-01)它会自动识别 hive 风格的分区目录读取分区字段时不需要实际把这列解析出来而是直接从目录路径推断。这个特性在日志类数据的分析中特别好用你不需要维护一份全量索引只要目录结构规范查询时加上分区过滤即可。3.5 查看元数据和 schema 的小技巧调试时我经常会先读取文件的 metadata而不去碰实际数据import pyarrow.parquet as pq parquet_file pq.ParquetFile(example.parquet) print(parquet_file.metadata) print(parquet_file.schema) print(parquet_file.metadata.row_group(0))这个习惯能帮你快速判断文件的分区数、每组的行数、压缩方式、是否包含统计信息。如果遇到读取慢先看 row group 大小和数量的配比再判断是不是 row group 太小导致数据块过多、扫描效率低。4. 内存优化和流式处理4.1 超大数据集的分批读取面对一个几十 GB 的 Parquet 文件直接pq.read_table会非常危险。先把文件读进内存如果此时内存只剩 16GB大概率会看到 MemoryError。更合理的方案是用迭代器式读取把数据按批次塞给下游逻辑import pyarrow.parquet as pq pf pq.ParquetFile(huge.parquet) for batch in pf.iter_batches(batch_size100_000): # 这里拿到的是 RecordBatch df batch.to_pandas() # 处理后立即释放下一轮继续iter_batches每次返回一批RecordBatch你可以在每批内部做聚合、落盘或者发到队列里处理完一批就释放一批。这样做能保证整体内存水位稳定在一个较低的水平不会像一次性读全表那样让你的内存峰值冲上云霄。4.2 使用 Arrow IPC 流式读写多个数据块除了 ParquetArrow 还有一套叫 IPC 的内存流格式在进程内、进程间传递数据时效率很高。如果你想在内存中连续把多张表拼在一起可以这样import pyarrow.ipc as ipc options ipc.IpcWriteOptions(compressionzstd) with ipc.new_file(batch.arrow, schematable.schema, optionsoptions) as writer: for batch in batches: writer.write_batch(batch) with ipc.open_file(batch.arrow) as reader: whole reader.read_all()Arrow IPC 文件格式虽然不如 Parquet 通用但读取速度极快适合作为中间临时文件。比如数据清洗流程里第一阶段清洗完第二阶段要做特征转换中间临时落盘成 Arrow IPC速度要比 Parquet 快不少省去了一套压缩解压的开销。4.3 内存映射真正意义上不占物理内存的读取mmap是操作系统提供的一种文件映射机制简单说就是把文件内容映射到进程地址空间程序读取时才真正把对应页面加载到物理内存中。pyarrow 支持这种读取方式import pyarrow.ipc as ipc with ipc.open_file(batch.arrow) as reader: table reader.read_all() with ipc.memory_map(batch.arrow) as reader: table_mmap reader.read_all()这两种方式代码看起来很像但memory_map方式避免了一次性把整个文件全部读入物理内存虚拟地址空间增加不代表真实内存立刻占满。当文件的某些部分没有被访问时操作系统根本不会把那些磁盘页加载进来。这个特性在多个进程并行读同一份大文件时尤其有用因为大家映射的是同一份文件物理内存里只需要一份缓存。我在一个特征工程项目里就是用memory_map同时开了 8 个 worker 进程各自读取同一个 Arrow IPC 文件内存占用比原来每个进程独立读一份副本下降了非常多整个训练数据加载的时间也大大缩短。4.4 批量处理时的内存回收陷阱在处理大规模数据时除了 pyarrow 本身的读取方式还有一个隐藏问题转换成 pandas 后内存并不会立刻释放因为 pandas 和 Python 的垃圾回收机制有滞后性。当你一个批次处理完建议显式把 DataFrame 变量删掉并调用gc.collect()import gc for batch in pf.iter_batches(batch_size100_000): df batch.to_pandas() # 做业务处理 del df gc.collect()这听起来有点暴力但在低内存环境里确实有效。还有一点batch本身也会占用内存处理完后它在下一轮就会被重新赋值覆盖不用每次手动删除但如果你在下游留下了对 batch 的引用那么内存就永远不会释放。这种引用泄漏问题在 Jupyter 里最隐蔽因为你可能在前一个 cell 间接触发了对变量的保存。5. 常见问题与排查技巧5.1 从 pyarrow 表转 pandas 后类型对不上这是最常遇到的问题之一。Arrow 里string类型转回 pandas 时默认是 Pythonstr对象并不是 pandas 的StringDtype。如果你发现一列数值型数据里面混入异常值或者 null 导致类型发生了变化可以通过table.to_pandas(types_mapperpd.ArrowDtype)来保持 Arrow 类型的语义import pyarrow as pa import pandas as pd table pa.table({a: [1, 2, None]}) df table.to_pandas(types_mapperpd.ArrowDtype) print(df.dtypes)不过要注意pd.ArrowDtype是 pandas 2.0 以后才引入的功能需要高版本的 pandas。如果你用的是老版本那就只能接受 pandas 侧的类型升级规则在读入后手动做一次astype避免数据在后续处理中出幺蛾子。5.2 时间戳类型和时区偏移Arrow 的timestamp(ns)转回 pandas 时默认不带时区信息而 pandas 默认的 datetime64 是带纳秒精度的。很多时候你在读取时发现时间差 8 小时其实是时区单位不统一造成的。我的做法是如果业务处理在中国时区写入表时统一转成 UTC读取后再在展示层添加时区。Arrow 在底层是按照类型系统来管理时区的通过给 schema 指定时区你可以保证每个列解析后的语义一致import pyarrow as pa schema pa.schema([ pa.field(event_time, pa.timestamp(us, tzAsia/Shanghai)), ])同时如果你不想处理时区就让整个管道统一用时间戳整数或 UTC 字符串传递最后展示时再统一格式化这样最不容易出错。5.3 columns 参数拼写错误或不存在时用pq.read_table(columns[naem])时如果传入的列名不存在pyarrow 会直接报错。这个报错信息很明确但我见过很多朋友在真实 Pipeline 里没把列名校对好导致查询失败。建议先通过pq.ParquetFile.schema拿到真实列名列表再做映射好在 Arrow 这边 schema 是全量信息不会出现 pandas 那种“列不存在却只是 NaN”的宽松行为。5.4 文件写入过程中的异常处理写 Parquet 时如果文件已经存在write_table默认是覆盖还是追加这个要看写入方式。pq.write_table(path)在文件存在时并不会自动追加而是直接覆盖整个文件。如果你将很多分块数据写进同一个 Parquet 文件应该使用write_to_dataset或ParquetWriter的批量写入模式而不是重复调用write_table否则后一次调用会把前一次的内容覆盖掉。以ParquetWriter为例import pyarrow as pa import pyarrow.parquet as pq schema pa.schema([ pa.field(id, pa.int64()), ]) with pq.ParquetWriter(batch.parquet, schema, compressionzstd) as writer: for i in range(100): table pa.table({id: list(range(i * 1000, i * 1000 1000))}) writer.write_table(table)这种写法保证了追加行为是可控的不会造成数据丢失。分配的 serverless 环境里如果文件写在临时目录也要注意路径清理避免磁盘打满。5.5 pyarrow 和其他库的版本兼容pandas 2.0 之后Arrow 已经成为 pandas 的可选后端之一。老项目里如果 pandas 版本过旧ArrowDtype不可用可能会导致类型转换时报错。另外Spark 在 PyArrow 可用时toPandas会优先走 Arrow 通道如果 pyarrow 版本和 Spark 版本不匹配可能出现“spark-arrow 不支持”这类隐含问题。所以遇到莫名报错先去查一下你的 pyarrow、pandas、spark 三者版本是否有已知冲突这是排查此类问题最快的方法。6. 组合拳一个真实的 ETL 场景复盘6.1 场景描述朋友有一个系统每天产生大约 5GB 的日志文件原始格式是 CSV。他们过去用 pandas 直接读每次跑到一半就内存报警后来改为用 pyarrow 读入后做列筛选和类型规整再写入按日期分区的 Parquet 文件下游查询直接走 DuckDB 或者 pyarrow Dataset。整个流程现在跑在 8GB 内存的机器上一天的数据处理时间从 40 分钟降到了 5 分钟左右。这个场景非常有代表性CSV 本身没有任何索引和统计信息用 pandas 读取时只能全量扫描且 CSV 里如果有很多冗余列内存就会被垃圾数据白白占满。而用 pyarrow 读取 CSV 时可以指定哪些列、哪些类型甚至跳过错误行处理体验完全不同。6.2 核心代码示例下面给出一个可复用的模板你完全可以拿它改造成自己的 ETL 脚本import pyarrow.csv as csv import pyarrow.parquet as pq import pyarrow.dataset as ds from datetime import date # 1. csv 大文件流式读取 reader csv.open_csv( raw_data.csv, read_optionscsv.ReadOptions(block_size64 * 1024 * 1024), parse_optionscsv.ParseOptions(invalid_row_handlerlambda x: skip), convert_optionscsv.ConvertOptions( column_types{ user_id: pa.int64(), event_time: pa.timestamp(s), } ), ) # 2. 分批处理 for batch in reader: # 这里做基本的清洗过滤无意义的行 table pa.Table.from_batches([batch]) table table.filter(pa.compute.field(user_id).is_valid()) # 3. 写入带分区的 parquet pq.write_to_dataset( table, root_pathwarehouse/, partition_cols[dt], compressionzstd, )在写真实脚本时需要注意invalid_row_handler需要返回一个csv.ErrorHandlerResult对象如果你只是想跳过直接返回()来代表“丢弃该行。不同版本对这个回调的类型要求有一定差异建议查一下当前版本的手册。6.3 数据分区字段的选择partition_cols可以传多个列一般把过滤频率最高的列放到最外层。比如日志类数据按dt做第一级分区user_id做第二级分区查询时可以快速裁剪到很小的数据范围。但也要注意分区数量不能过多否则目录数量膨胀小文件碎片化问题会让 HDFS 或者对象存储的读写效率反而变差。一个合理的分区粒度比如按天、按小时远比按分钟稳妥。6.4 与查询下层的组合数据落地到 Parquet 分区目录之后下游如果使用 DuckDB、ClickHouse 或者 Spark都能直接把这些分区文件当作一张逻辑表来查询。pyarrow Dataset 本身就是这类查询引擎的元数据来源之一只要你文件里没有脏数据schema 统一下游接入成本极低。我就常用 DuckDB 直接查询 pyarrow 生成的 Parquet 目录DuckDB 内部读取 Parquet 时会自动做并行扫描和谓词下推几十 GB 的数据查询响应时间基本在亚秒级别。这也解释了为什么 Arrow 在分析型生态里越来越常见它把存储、内存、查询环节统一到了同一种列式语言之下。7. 这类库真正的价值和使用边界7.1 什么时候强烈建议使用 pyarrow说实话不是所有场景都需要 pyarrow。如果你的数据只有 20 万行pandas 完全能应付那折腾 Arrow 可能得不偿失。但下面几种情况强烈建议你认真考虑 pyarrow数据量在几个 GB 以上pandas 已经出现明显的读取压力和内存压力。需要频繁在多个系统之间交换数据比如外部数据源进 Python 再进数据库。写入或读取 Parquet、Arrow IPC 等列式格式。做数据管道时需要长期维护一套稳定的 schema 和数据类型定义。需要并行处理Arrow 的线程执行模型和内存池设计在并行扫描时效果明显。7.2 什么时候它并不是最佳答案如果你的痛点在于数据量只有几万行、且所有下游都只认 pandas那么继续用 pandas 就行。还有一种情况是你处理的数据虽然大但你已经用了 Spark 或 Flink 这类引擎做分布式计算那么 pyarrow 更多是作为引擎内部的一个存储层存在而不是你日常主操刀的工具。pyarrow 的另一个边界是它本身不是一个计算引擎不擅长做非常复杂的关系代数下推。虽然 Arrow 也有pyarrow.compute提供的过滤、聚合等函数但复杂的 SQL 查询还是交给 DuckDB、DataFusion 或者 Spark 更合适。Arrow 扮演的角色更接近弹药库和运输车它把数据准备好、搬得快真正算账的还是那些计算引擎。7.3 从实践角度来看的总体判断我个人对 pyarrow 的评价是它是一个“用了就回不去”的库。刚开始它可能要逼你适应新的数据类型、新的 schema 思维但只要你度过第一天的适应期后续在内存、速度和跨系统数据交换上获得的收益会远超当初的学习成本。最后再分享一个小技巧如果你要把一个超大的 pandas DataFrame 一次性存到磁盘又不想让后续读取时内存爆炸我的习惯是先把它切成若干个 RecordBatch再分批写入 Parquet最后用 Dataset API 去统一读。整个过程并不复杂核心逻辑就是用 Arrow 的ChunkedArray和RecordBatch思维去对待大表而不是默认为“全表常驻内存”。如果你现在手上的项目正好被大文件读取卡住与其换个更大的机器不如先试试 pyarrow 的列裁剪、谓词下推和分批读取这三个功能。我见过太多数据量其实没有想象中大、却被错误读取方式拖垮的例子pyarrow 就是这么一类能帮你把机器性能重新找回来的库。踩过几次坑之后你会发现这些优化点基本都是相通的能晚一步读数据就晚一步读能少读一列就少读一列能压缩就压缩内存就不是瓶颈了。