ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DuckDB大Parquet文件采样防OOM:水库采样与TABLESAMPLE实战

DuckDB大Parquet文件采样防OOM:水库采样与TABLESAMPLE实战 1. 一次读个文件引发的内存崩溃事故上个月我接到一个有点特殊的活项目方给了一份 11.2GB 的 Parquet 文件里面存的是某水库连续三年的水质监测记录大概 3400 万行字段有站点编号、采样时间、水温、pH、浊度、溶解氧这些。需求听起来很简单——从这个大文件里均匀抽出 1 万条记录作为后续统计分析和模型训练的样本。我一开始压根没把这事儿当回事。DuckDB 读取 Parquet 本来就是它的招牌能力一条SELECT * FROM xxx.parquet就能直接查。可当我真正动手写查询的时候事情开始不对劲了。第一次我图省事写了SELECT * FROM monitor_data.parquet LIMIT 10000;这条 SQL 秒出结果但拿到手的显然只是文件前 1 万行。这份数据的采集点位是按时间顺序排的前半年的记录和后半年的记录在温度、浊度这些指标上大概率有显著差异。直接取前 1 万行等于把样本变成了上半年快照完全谈不上均匀更别说什么水库采样了。于是我把查询改成ORDER BY random() LIMIT 10000想靠随机排序来均匀抽样。结果这条 SQL 一跑服务器内存肉眼可见地往上窜htop 里那个进程占用的内存从 2GB 一路涨到 14GB然后整个 Python 进程被 Linux 的 OOM Killer 直接干掉了。同一时间我的 SSH 会话也卡到几乎没法操作——DuckDB 默认会尽量吃满物理内存留给系统的余量被压缩得所剩无几。崩溃之后我查了dmesg日志里写着Out of memory: Killed process 25418 (python3)。在容器里可能表现为 exit code 137在裸机上就是进程直接被 kill。这种死法最难受的地方在于没有堆栈、没有日志、没有到底是哪一行代码出错你只知道自己把服务器的内存彻底榨干了。现在回头看这个问题的本质不是 DuckDB 不行而是我不了解 DuckDB 在内存管理和 Parquet 扫描上的工作方式。标题里的水库采样其实对应的就是 reservoir sampling——蓄水池采样算法它是从大文件中均匀抽取样本且不撑爆内存的正解也恰好成了这次任务的最终解法。这篇文章就把完整的排查思路、水库采样原理和落地方案写出来给同样被大 Parquet 文件 内存崩溃折磨过的人一个参考。2. 根因追踪DuckDB 到底把内存花在哪里了2.1 先看懂 DuckDB 的内存模型DuckDB 本质是嵌入式分析型数据库它设计上就跟 SQLite 类似进程内运行没有独立服务端。这也意味着它跟你写的 Python 脚本共享同一个进程地址空间它吃内存就是你的进程在吃内存。DuckDB 内部有一套 Buffer Manager 管理缓冲池。简单理解就是所有需要处理的数据页面都要先加载到这个缓冲池里算子再从池子里拿数据算。而缓冲池的大小不是无限大的默认上限是物理内存的 80%——这是 DuckDB 的初始默认值memory_limit。这个 80% 很关键。在一个 16GB 内存的服务器上DuckDB 自认为最多能用 12.8GB。如果你的机器上还跑着操作系统、SSH、编辑器、Python 解释器实际可用内存根本到不了 12.8GB。一旦查询请求的内存超过这个数会有两种结果一是 DuckDB 自己抛出Out of Memory Error: Failed to allocate二是操作系统直接 OOM Kill。我遇到的是后者因为 DuckDB 在尝试申请内存的时候操作系统已经扛不住了先动手把进程杀掉了。知道了这个机制第一个排查动作就很明确了查看当前 DuckDB 的连接配置确认memory_limit和threads到底是多少。-- 查看当前 DuckDB 的配置 SELECT * FROM duckdb_settings() WHERE name IN (memory_limit, threads, temp_directory);我这边查出来的结果就是memory_limit 约 14.9GB80% 的 18.7GB 物理内存threads 16。也就是说我那台机器虽然物理内存看起来不小但 DuckDB 一口气把 16 个线程都拉起来跑每个线程又有自己的执行上下文内存墙一下就撞上了。2.2 Parquet 文件格式对内存消耗的推波助澜Parquet 是列式存储格式这本来是它的优点查询只用某几列时可以跳过无关数据块。但它有一个特性只要踩中就会放大内存问题——列数据是按 Row Group 组织的读取时会对整个 Row Group 内的列 chunk 进行解压和物化。举个例子我那个文件里有一列是溶解氧字段类型是 DOUBLE一个 Row Group 默认大约是 122880 行。DuckDB 扫描的时候会把这一列的一个 chunk 解压后放到 Buffer Manager 里。单独看一个 chunk 并不大但如果是SELECT *每个 Row Group 的全部列 chunk 都会被读进来。我的表有 8 个字段等于一个 Row Group 就要同时物化 8 个列数组内存涨得自然快。更要命的是Parquet 文件如果由写入方设置成很大的 Row Group Size比如 500 万行一组那每次物化就是 500 万行的列数据。再加上 DuckDB 为了提高吞吐会预读多个 Row Group内存占用量会更进一步被顶高。这也是为什么不少人在本地开发机上读一个 2GB 的 Parquet 也会卡顿的原因之一——不是 DuckDB 效率低而是它把整列整块的数据都搬到内存里了省的是磁盘 I/O花的是内存。2.3 真正引爆内存的是哪种操作但话说回来单纯扫描一个 11GB 的 ParquetDuckDB 未必会 OOM因为它有流式扫描机制可以读一批、算一批、扔一批。真正让我崩溃的是那条ORDER BY random() LIMIT 10000。这条 SQL 表面上是随机抽 1 万行实际上它的执行计划是先扫描整张表对每一行调用random()函数生成一个随机键把随机键 整行数据全部丢进排序算子对整表按随机键做全局排序排完序之后取前 1 万行返回。问题出在第 2 步和第 3 步。排序不是流式操作至少不是天然的流式——它需要把参与排序的数据全部缓存下来才能决定顺序。如果你想排序全表 3400 万行那这 3400 万行的随机键和行指针都要占内存。DuckDB 的 Sort 算子默认是能在内存放就放内存超过memory_limit才会考虑落盘但前提是你显式配置了temp_directory。我没配置于是它就硬着头皮一直吃内存直到把系统吃穿。还有一种操作也容易踩雷SELECT * FROM big.parquet之后直接.df()转成 pandas DataFrame。DuckDB 的结果集本身在内存里已经有一份了pandas 又复制一份瞬间翻倍。大文件场景下这是一条自杀路径。2.4 我用的排查手段崩溃以后我分了三步确认元凶第一步查dmesg | grep -i killed process确认是 OOM Killer 动手而不是 DuckDB 自己报错。这一步帮我排除了语法问题。第二步用EXPLAIN ANALYZE跑了一遍出问题的 SQL重点看每个算子的内存估计和返回行数EXPLAIN ANALYZE SELECT * FROM monitor_data.parquet ORDER BY random() LIMIT 10000;执行计划里ORDER BY节点前面挂着一个全表扫描计划输出里它的累积行数是 3400 万行。看到这个数字我心里就有数了这条查询无论如何都要处理全表排序算子的 peak memory 一定会和表大小量级相同。第三步做了个对比实验SELECT COUNT(*) FROM monitor_data.parquet几乎是秒回。因为 DuckDB 读 Parquet 文件时会从 footer 元数据里直接拿到 row group 级行数纯计数不需要扫描数据页。这让我意识到DuckDB 并不是每次都要把全文件读爆是random()这种不可下推、不可裁剪的操作逼着它做全量扫描。排查到这里问题的本质已经很清楚了我需要的不是全表排序而是从全表流式扫描中做一次均匀抽样让内存只跟样本量挂钩而不是跟全表行数挂钩。这正是水库采样算法的用武之地。3. 水库采样为什么它能以小博大3.1 算法步骤一个水桶的换水游戏蓄水池采样Reservoir Sampling中文常被直译为水库采样要解决的是这样一个问题你面对一个不知道总行数的数据流没法一次性放进内存但你需要从中均匀抽出 k 个样本并且保证这 k 个样本与遍历完整条数据流后随机抽 k 个在概率上完全等价。算法本身朴素到让人惊讶准备一个大小为 k 的水库数组 reservoir把数据流的前 k 个元素挨个放进水库从第 k1 个元素开始每读到一个新元素生成一个随机整数 j范围是 [0, 当前已读到的元素个数)如果 j k就用新元素去替换水库中下标为 j 的那个元素如果 j k这个新元素直接被丢弃遍历完所有数据后水库里剩下的 k 个元素就是均匀抽样结果。我用 Python 写了一下标准实现。假设已经有一个流式数据源streamimport random def reservoir_sample(stream, k: int): reservoir [] seen 0 for item in stream: seen 1 if len(reservoir) k: reservoir.append(item) else: # 随机生成 [0, seen) 之间的整数 j random.randint(0, seen - 1) if j k: reservoir[j] item return reservoir你可能会觉得这不就是把前面一部分数据先占住位置后面随机替换吗怎么就能保证均匀答案是数学。3.2 均匀性证明一次归纳就能想明白用归纳法证明。假设我们已经处理完前 i 个元素i k且当前水库中的 k 个元素是从前 i 个元素中均匀抽取的也就是每个元素留在水库的概率都是 k/i。现在来了第 i1 个元素。按照算法新元素被选入水库的概率是 k/(i1)。这个概率怎么来的因为随机整数 j 的范围是 [0, i1)总共有 i1 个取值其中前 k 个取值0 到 k-1会触发替换所以入选概率就是 k/(i1)。那水库里原来的旧元素呢对任意一个旧元素 x它被替换掉必须同时满足两个条件新元素被选中概率 k/(i1)并且随机到的那一格恰好是 x 所在的位置概率 1/k。两个独立事件乘起来x 被替换的概率是 (k/(i1)) × (1/k) 1/(i1)。所以 x 在第 i1 步之后仍然留在水库的概率等于第 i 步结束时它在水库里的概率乘以第 i1 步没被替换的概率(k/i) × (1 - 1/(i1)) (k/i) × (i/(i1)) k/(i1)完美前 i1 个元素中每个元素留在水库的概率都收敛到了 k/(i1)。由归纳法可知当整个数据流读完 n 个元素时水库中的每个元素都是等概率的 k/n。这就是水库采样的均匀性来源。说句题外话这个证明我第一次看到时觉得它简单得不像话但背后那种只用一个随机数就颠覆了排序思路的直觉恰恰是它在大数据场景下有生命力的原因。你完全不需要知道 n 是多少也不需要把 n 个数据存下来。3.3 内存占用与排序采样的本质区别水库采样的内存占用是 O(k)k 是样本量。在我这个场景里 k 10000也就是说整个采样过程只需要保存 1 万行数据外加一个不断替换的循环。而ORDER BY random() LIMIT的内存占用是 O(n)n 是全表行数。就算你最后只取 1 万行排序过程中也要先把全表 3400 万行的随机键都排好内存量级差了几千倍。用一句话总结两者的区别排序采样的思路是先排完全部再取前头水库采样的思路是边走边换最后剩下的就是样本。前者受限于 n后者只受限于 k。3.4 与其他采样方式的横向对比DuckDB 里常见的采样方式有这么几种我把它们放在一起比较方式内存量级采样均匀性返回行数适用场景LIMIT nO(1)极低很差只取开头固定 n快速看几条数据ORDER BY random() LIMIT nO(n)全表排序均匀固定 n小表可用大表灾难TABLESAMPLE BERNOULLI(p)O(流式)低均匀逐行独立概率不固定约为 p%需要随机比例、不要求精确行数TABLESAMPLE SYSTEM(p)O(块级)最低较差按数据页或 row group 跳块不固定超大表极速抽样能接受块级偏差TABLESAMPLE RESERVOIR(k)O(k)与样本量相关均匀精确 k 行精确抽 k 条且不想接触全量排序BERNOULLI和RESERVOIR看起来很像都基于随机数。但BERNOULLI是逐行独立判断留还是不留保留概率固定为 p所以最终行数有波动RESERVOIR则保证最终恰好返回 k 行。如果你下游的流程是抽 1 万条去训练模型少一条都会报错那就必须用RESERVOIR。4. DuckDB 落地水库采样三种姿势与实测对比4.1 姿势一直接用 TABLESAMPLE RESERVOIR推荐DuckDB 内置了TABLESAMPLE语法支持BERNOULLI、SYSTEM、RESERVOIR三种采样方法。我要找的其实就是它。直接读 Parquet 文件采样SELECT * FROM monitor_data.parquet TABLESAMPLE reservoir(10000 ROWS);如果我只关心均匀性不关心精确行数也可以拿百分比SELECT * FROM monitor_data.parquet TABLESAMPLE reservoir(0.03%);这两种写法在 DuckDB 1.3 及更高版本上都实测可用。TABLESAMPLE放在表名或文件路径后面语义是对该表进行采样再把采样结果交给后续算子。在 Python 里集成可以这样写import duckdb conn duckdb.connect() conn.execute(SET memory_limit 10GB) df conn.execute( SELECT * FROM monitor_data.parquet TABLESAMPLE reservoir(10000 ROWS) ).df() print(df.shape) # (10000, 8)跑完的那一刻我盯着htop看了半分钟内存峰值只有不到 2GB跟之前 14GB 的悬崖式增长比简直是从地狱回到了人间。文件扫描时间大约 43 秒绝大多数时间花在读盘和解压上采样逻辑本身几乎没增加什么负担。4.2 姿势二Python 手写流式水库采样如果你的环境和需求比较特殊——比如要用到分层采样、加权采样或者你用的 DuckDB 版本较老不支持TABLESAMPLE RESERVOIR——那就退一步自己写。核心思路是用 DuckDB 的流式结果集逐批读取然后套用第三节那个reservoir_sample函数。import duckdb import random K 10000 reservoir [] seen 0 conn duckdb.connect() conn.execute(SET memory_limit 10GB) conn.execute(SET threads 8) cur conn.execute(SELECT * FROM monitor_data.parquet) while True: batch cur.fetchmany(50000) if not batch: break for row in batch: seen 1 if len(reservoir) K: reservoir.append(row) else: j random.randint(0, seen - 1) if j K: reservoir[j] row print(f共扫描 {seen} 行最终保留 {len(reservoir)} 行)这里有一个细节必须提醒fetchmany(50000)是流式逐批读取DuckDB 不会把整个结果集一次性物化到 Python 层。我实测下来内存峰值稳定在 每次批次大小 K 行样本 DuckDB 扫描缓冲 的范围内同样不会爆。代价是速度。因为每一行都要在 Python 的 for 循环里过一遍3400 万行的处理时间大约在 4 到 6 分钟跟内置TABLESAMPLE的 43 秒不是一个量级。但它的灵活性上限更高你想做分层抽样可以按station_id拆成多个子流分别采样最后合并。你想做有时间窗口约束的采样可以在循环里加条件判断。这是内置语法做不到的。4.3 姿势三窗口函数开窗采样的伪方案还有人在网上推荐用窗口函数做采样比如SELECT * FROM ( SELECT *, row_number() OVER (ORDER BY random()) AS rn FROM monitor_data.parquet ) WHERE rn 10000;我必须直说这就是ORDER BY random() LIMIT换了件马甲。它依然要对全表做随机排序row_number()窗口算子的内存开销一点不比ORDER BY小。在优化器眼里这条 SQL 的执行计划跟你直接写ORDER BY random() LIMIT 10000没有本质区别。试都不用试直接划掉。还有一个不太常见但有人用过的烂招先SELECT * FROM file查出全表在 Python 里拿到行数 n再随机生成 10000 个不重复的行号最后用行号去回查表。这种方式需要两次全表扫描第二次回查做嵌套循环 join内存和时间都不可控也不推荐。4.4 三个方案的实测对比在同样的环境里16 线程、16GB 内存、11.2GB Parquet、3400 万行我把四个方案都跑了一遍结果如下表方案内存峰值耗时是否均匀最终结论LIMIT 10000约 0.8GB2 秒否不满足需求ORDER BY random() LIMIT 10000超过 14GB卡死后被 OOM 杀—不可用TABLESAMPLE reservoir(10000 ROWS)约 1.8GB43 秒是首选Python 手写流式水库采样约 1.5GB约 5 分钟是灵活场景用这个结果让我彻底明白了在 DuckDB 里做大规模采样TABLESAMPLE RESERVOIR就是为这个场景量身定做的语法不需要自己造轮子。5. 配套防守让大 Parquet 读取不再爆内存的工程配置水库采样解决的是抽样这个核心问题。但它不是万能药——如果你的需求不是采样而是需要全量聚合、关联或者导出那就得靠另一套手段给 DuckDB 戴上紧箍咒。5.1 显式设置 memory_limit别让 DuckDB 自己说了算DuckDB 默认把 80% 物理内存当可用内存在用。在裸机上这也许还行但跑在 Docker 容器里就非常危险——容器看到的内存限制和宿主机物理内存完全是两回事。一个只分配了 4GB 的容器DuckDB 若按宿主机 32GB 的 80% 配置 memory_limit一跑复杂查询立刻撞上 cgroup 的上限进程直接被杀。所以我现在的习惯是每次建连接后第一件事就是显式限制内存conn.execute(SET memory_limit 6GB) conn.execute(SET threads 4)threads也值得限制。DuckDB 默认起满所有 CPU 核心每个线程的执行上下文、排序缓冲、扫描缓冲都要占内存。把线程数从 16 降到 4内存峰值能下降三到五成代价是查询时间上升一些但换来的稳定性非常值得。在共享服务器上这还是一种基本的资源礼貌。5.2 配置 temp_directory让排序和聚合有退路ORDER BY、GROUP BY、JOIN这类算子内存耗尽时并不会自动落盘。只有你设置了临时目录DuckDB 才会把中间结果溢写到磁盘避免 OOM。SET temp_directory /data/duckdb_temp;这个目录务必放到有大块磁盘空间的位置别放系统根目录也千万别放内存盘——那就等于绕了一圈又回到内存里去了。我见过有人把 temp_directory 配到/tmp然后遭遇根分区写满、整个系统挂掉的惨案。临时文件的大小可能达到数据量的 0.5 到 2 倍提前用df -h确认剩余空间是基本操作。要注意落盘不等于性能无损。溢写走的磁盘 I/O 会让查询慢一个量级。所以temp_directory是保命手段不是提速手段。能用采样、裁剪解决的问题别指望靠落盘硬扛。5.3 列裁剪与谓词下推能少读一列就少读一列Parquet 是列式存储它的最小读取单元不是文件而是某一列的某个 Row Group 块。因此只 SELECT 你真正需要的列能明显减少读入内存的数据量。SELECT station_id, sample_time, temperature, ph FROM monitor_data.parquet TABLESAMPLE reservoir(10000 ROWS);我只取 4 列而不是SELECT *的 8 列扫描和解压的字节数直接砍半。如果你做的是统计模型训练上游特征列就那几个这招的效果来得最快。谓词下推同理。Parquet 每一层 Row Group 的 footer 里都存了该块的min/max统计信息DuckDB 可以利用它做 Row Group 级别的裁剪。类似这样的查询SELECT station_id, sample_time, temperature FROM monitor_data.parquet WHERE sample_time DATE 2024-01-01 AND sample_time DATE 2024-07-01 TABLESAMPLE reservoir(5000 ROWS);DuckDB 会在扫描阶段跳过大量不满足时间条件的 Row Group真正进入内存的数据可能只有原来的三四成。注意先过滤再采样还是先采样再过滤结果语义完全不同。上面这个写法是先过滤再采样吗实际上不是SQL 的执行顺序是 TABLESAMPLE 作用于表之后才到 WHERE 吗这里细节要谨慎我在后面踩坑清单里专门说。5.4 Row Group 数量与多文件拆分还有一个容易被忽视的点Parquet 文件的 Row Group 大小和数量直接决定了 DuckDB 扫描时的内存粒度。如果文件是 一个超大 Row Group 的结构DuckDB 读一个块就要物化上千万行的列数据内存瞬间紧绷。反过来如果 Row Group 是 50 万到 100 万行一组DuckDB 可以更细粒度地流式处理。写入端通常用parquet_compression、row_group_size参数来控制。比如用 pyarrow 写数据时import pyarrow as pa import pyarrow.parquet as pq pq.write_table( table, output.parquet, row_group_size1_000_000, compressionzstd, )如果你的数据源本身就是一堆分片文件直接用 glob 方式读取让 DuckDB 把多个 Parquet 当作一张大表处理SELECT * FROM water_monitor/*.parquet TABLESAMPLE reservoir(10000 ROWS);多文件 均匀采样的组合在 DuckDB 里工作得很好因为水库采样的流式本质让它可以逐个文件、逐个 Row Group 地处理内存占用始终维持稳定。6. 踩坑记录与最终建议6.1 踩坑一TABLESAMPLE 与 WHERE 的执行顺序我这人有个坏毛病写完 SQL 喜欢凭直觉猜执行顺序。第一次用TABLESAMPLE RESERVOIR时我写的是SELECT * FROM monitor_data.parquet TABLESAMPLE reservoir(10000 ROWS) WHERE station_id SW-01;我以为自己是在先筛出某个站点再抽 1 万条但实际执行顺序是先对全表做水库采样抽 1 万行再对这一万行做 WHERE 过滤。最后可能只剩 700 行我差点带这个错误结果去交活。SQL 的语义里FROM 子句先决定行来源WHERE 是对 FROM 结果的过滤。TABLESAMPLE挂在 FROM 子句的表后面它的采样发生在 WHERE 之前。想要过滤后再采样要把过滤子查询包一层SELECT * FROM ( SELECT * FROM monitor_data.parquet WHERE station_id SW-01 ) AS filtered TABLESAMPLE reservoir(10000 ROWS);这个细节坑了我大半个下午写出来希望能帮你省下这点时间。6.2 踩坑二采样后再排序别让 ORDER BY 又把内存引爆拿到 1 万行样本之后我顺手加了个ORDER BY sample_time想按时间排序再导出。这时候的 1 万行排序内存开销跟 3400 万行的排序完全不是一个数量级没有风险。但如果你把TABLESAMPLE和ORDER BY LIMIT写反了-- 错误示例 SELECT * FROM monitor_data.parquet ORDER BY random() TABLESAMPLE reservoir(10000 ROWS); -- 语法也不对逻辑更不对那等于又回到全量排序的路线了。记住一句话先采样再做任何物化操作。采样之后的数据量已经从千万级降到万级接下来的任何操作都翻不起浪花。6.3 踩坑三.df()的隐性全量物化还有一个更容易被忽视的坑在 Python API 里# 危险写法 all_data conn.execute(SELECT * FROM monitor_data.parquet).df() sample all_data.sample(n10000).df()会把 3400 万行全部物化成 pandas DataFrame。这时候内存里同时存在 DuckDB 的结果集和 pandas 的副本双双叠加16GB 直接见底。正确的做法是把采样下沉到 DuckDB SQL 层只把最终 1 万行转成 DataFrame。所有抽样逻辑能下沉就下沉Python 层只保留最终结果。6.4 踩坑四容器环境下的内存感知错位最后提醒一下跑在 Docker 里的朋友。DuckDB 读取物理内存时走的是系统调用它看到的往往是宿主机内存总量而不是容器 cgroup 的限额。一个内存限额 4GB 的容器DuckDB 默认 memory_limit 可能是宿主机 32GB 的 80%这等于一开始就埋了雷。正确做法是让 DuckDB 的内存上限跟容器配额对齐docker run --memory4g --oom-kill-disablefalse ...然后在代码里显式设置SET memory_limit 3GB; SET threads 4;宁可让 DuckDB 因为内存不足抛错也好过它把容器搞 OOM 后所有日志都找不到。内存限制是保护机制不是性能瓶颈别不舍得给。6.5 一些操作层面的心得折腾完这套方案我自己总结了几条可以复用的经验第一遇到大文件 采样需求第一反应应该是搜一下数据库内置语法而不是自己写复杂查询。DuckDB 的TABLESAMPLE RESERVOIR一行顶过我手写一百行这属于站在官方文档肩膀上的胜利。第二任何时候怀疑内存问题先看 DuckDB 的memory_limit和threads再看执行计划是不是有全表排序或全表物化。这两个信息往往五分钟就能定位问题。第三临时目录是刚需。不管你要不要落盘先设置好temp_directory它给排序、聚合、JOIN 留了一条退路。这个配置相当于给数据库买了份意外险平时用不上最好用上的时候能救命。第四数据量大到一定程度别硬刚。如果任务只是抽样 1 万条那根本没必须读取全表后排序——算法早就给了答案工具也早就给了语法缺的只是意识到水库采样这个思路的那一瞬间。我这次踩坑最大的收获不是学会了TABLESAMPLE而是重新理解了以少量内存换流式处理这个工程哲学。现在这份 11.2GB 的文件我已经能在一分钟内稳定抽出任意行数的均匀样本而且内存占用始终维持在 2GB 以内。往后团队里谁再遇到大 Parquet 读取崩溃的问题我都是先把这篇里写到的排查链路发过去先看 memory_limit再看执行计划最后找采样算法。十有八九能对症下药。
返回列表