DuckDB 分析 10GB 日志 CSV把 pandas 30 分钟查询压到 8 秒我只做了这三步说实话我最近被一个 10GB 的 Nginx 访问日志搞得很烦。业务要做一个“最近 30 天接口访问热度”的临时分析数据导出来是 10GB 的 CSV。我用 pandas 跑了几次第一次直接 OOM第二次加了dtype和chunksize分段处理跑了 30 分钟才出结果中间还卡死两次。同事在旁边说“要不你直接写 Hive SQL 吧”问题是我就一个人本地想查个数据犯得着上集群吗后来我想起 DuckDB 这个嵌入式分析型数据库。它不需要服务器一个 Python 包就能跑还能直接读 CSV/Parquet。结果很离谱同样的查询pandas 30 分钟DuckDB 8 秒。我把过程整理成三步方便你下次也直接抄作业。问题背景为什么 pandas 在这里这么慢我的日志文件格式很常规timestamp,method,path,status,response_time,remote_ip,user_agent 2026-07-15T10:00:0008:00,GET,/api/user,200,23,1.2.3.4,Mozilla/5.0...10GB 大概 1.2 亿行。我要做的其实不复杂按path分组统计 PV按remote_ip统计 Top 20 来源 IP按status分组看 4xx/5xx 占比算response_time的 P95/P99pandas 的瓶颈主要在两点它默认会把整个 CSV 读到内存光加载就要吃 40GB 左右内存。字符串列在 pandas 里开销很大聚合时临时对象又多CPU 和内存一起爆炸。不是 pandas 不行是它定位就不是给“单节点分析 10GB 日志”设计的。DuckDB 天生 columnar、向量化、支持直接读取外部文件明显更适合这个场景。第一步CSV 转 Parquet压缩和结构一起搞定DuckDB 可以直接读 CSV但日志分析是反复查询最好先把 CSV 转成 Parquet。Parquet 是列式存储压缩率高读取时还能只读需要的列。我用了一个 Python 脚本做转换importduckdb conduckdb.connect()con.execute( COPY ( SELECT strptime(timestamp, %Y-%m-%dT%H:%M:%S%z)::timestamp AS ts, method, path, status::smallint AS status, response_time::integer AS response_time, remote_ip, user_agent FROM read_csv_auto(nginx_logs.csv) ) TO nginx_logs.parquet (FORMAT PARQUET, COMPRESSION ZSTD) )这段代码有两个关键细节用strptime把带时区的字符串转成timestamp后面按时间过滤不会出错。status和response_time显式转成 smallint/integer避免 DuckDB 把数字当成大字符串处理。转换耗时大概 1 分 40 秒10GB CSV 压成了 1.4GB 的 Parquet。不是 pandas 那种“读完再写”的过程DuckDB 是一边读 CSV 一边写 Parquet内存基本稳定在 1GB 左右。第二步用 SQL 直接查 Parquet聚合快到离谱转换完之后查询就非常爽了。DuckDB 支持read_parquet直接读取文件不需要导入表。importduckdb conduckdb.connect()# Top 20 接口 PVcon.execute( SELECT path, count(*) AS pv FROM read_parquet(nginx_logs.parquet) GROUP BY path ORDER BY pv DESC LIMIT 20 ).fetchdf()# P95/P99 响应时间con.execute( SELECT quantile_cont(response_time, 0.95) AS p95, quantile_cont(response_time, 0.99) AS p99 FROM read_parquet(nginx_logs.parquet) ).fetchdf()# Top 20 来源 IPcon.execute( SELECT remote_ip, count(*) AS cnt FROM read_parquet(nginx_logs.parquet) GROUP BY remote_ip ORDER BY cnt DESC LIMIT 20 ).fetchdf()这三个查询加起来在我这台 16GB 内存的笔记本上跑了 8.2 秒。作为对比pandas 单查询groupby那一步就跑了 12 分钟以上。第一次 OOM 之后我顺手记了下对比数据方案加载时间聚合时间峰值内存文件大小pandasread_csv groupby约 8 分钟约 22 分钟41GB10GB CSVpandaschunksize分段聚合约 5 分钟约 25 分钟12GB10GB CSVDuckDB CSV→Parquet SQL约 1 分 40 秒约 8 秒1.2GB1.4GB Parquetpandas 分段聚合虽然内存下来了但代码要写多线程合并复杂度直线上升。DuckDB 这边基本就是写 SQL省下来的时间够我喝杯咖啡。顺手加了几个时间维度的查询业务不只是看总数还想看每天 4xx/5xx 的变化趋势。用 DuckDB 也很简单con.execute( SELECT date_trunc(day, ts) AS day, status, count(*) AS cnt FROM read_parquet(nginx_logs.parquet) WHERE status 400 GROUP BY day, status ORDER BY day DESC, cnt DESC ).fetchdf()date_trunc这种时间函数不需要额外导入 datetime直接写 SQL 就行。再搭配quantile_cont算分位list/unnest展开数组基本常见的日志分析需求都能覆盖。我还顺手写了一个环比查询把最近 7 天和上一周的接口 PV 做对比找出涨幅超过 30% 的接口。DuckDB 支持 CTESQL 结构和在 PostgreSQL 里写的几乎一样迁移成本很低。临时查一次可以但如果每天都想跑一遍最好封装起来。我把上面这些固定成了analyze_logs.pyimportduckdbimportsys LOG_PATHsys.argv[1]iflen(sys.argv)1elsenginx_logs.parquetconduckdb.connect()# 限制最大内存防止小机器被拖死con.execute(SET memory_limit 4GB)con.execute(SET threads 4)print( 接口 PV Top 20 )print(con.execute(f SELECT path, count(*) AS pv FROM read_parquet({LOG_PATH}) GROUP BY path ORDER BY pv DESC LIMIT 20 ).fetchdf().to_string(indexFalse))print(\n 状态码分布 )print(con.execute(f SELECT status, count(*) AS cnt, round(count(*) * 100.0 / sum(count(*)) over (), 2) AS pct FROM read_parquet({LOG_PATH}) GROUP BY status ORDER BY status ).fetchdf().to_string(indexFalse))print(\n 响应时间分位 )print(con.execute(f SELECT round(quantile_cont(response_time, 0.50), 2) AS p50, round(quantile_cont(response_time, 0.95), 2) AS p95, round(quantile_cont(response_time, 0.99), 2) AS p99 FROM read_parquet({LOG_PATH}) ).fetchdf().to_string(indexFalse))加memory_limit和threads是为了让它在笔记本上也能稳一点。如果你的机器更强可以把线程数调大。它不会像 pandas 那样冷不丁把内存吃光然后被系统 OOM killer 干掉。踩坑记录时区字符串解析容易出错我原始日志的时区格式是08:00strptime的格式串要写%Y-%m-%dT%H:%M:%S%z。如果直接read_csv_auto后按时间过滤时区会识别成字符串结果全错。CSV 里有脏数据要处理有些行的response_time是-或者空值DuckDB 默认会报错。可以在read_csv_auto里加nullstr[, -]和columns{...}或者用try_cast容错。Parquet 文件路径含通配符如果是按天拆分的日志比如logs/2026-07-*.parquet可以直接用read_parquet(logs/*.parquet)DuckDB 会自动并行读多个文件。DuckDB 版本差异0.10 之前的read_csv_auto对类型推断比较激进建议升级。我用的是 1.0.0稳定很多。不是银弹这些情况我还是选 ClickHouse 或 pandasDuckDB 虽然香但不是所有场景都合适数据超过 100GB 或者需要持续实时写入这种规模 DuckDB 单节点会比较吃力ClickHouse、Doris、Snowflake 这类真正的分布式 OLAP 更合适。要做复杂的特征工程、数据清洗流水线pandas 的 DataFrame 操作更灵活结合 DuckDB 的fetchdf()把结果拉出来再用 pandas 处理也是不错的选择。团队协作需要权限和元数据管理DuckDB 是本地文件没有用户权限、Schema 版本管理多人协作时还是得上数据库服务。我的做法是把 DuckDB 当成“本地超大数据文件分析器”导入、聚合、出数然后结果再回到 pandas 做可视化或者业务层使用。两个工具并不冲突选对场景才是关键。DuckDB 不是来替代 ClickHouse 或 Snowflake 的它真正的定位是你本地有一个几十 GB 的数据文件想快速用 SQL 查一遍不想搭服务也不想 pandas 折磨你。如果数据量超过 100GB 或者需要实时写入那还是得看真正的 OLAP 集群。但如果你像我一样经常被“临时导出的 CSV 日志”和“pandas 跑不动”之间的鸿沟卡住那 DuckDB 真的是被低估的神器。安装也简单一行命令就能搞定pipinstallduckdbDuckDB 还有官方的命令行工具duckdb如果你不想写 Python直接在 shell 里跑 SQL 也可以。我有时候临时查数就用 CLI比起一个 Python 脚本更省事。我把上面的转换脚本和分析脚本都整理好了复制过去改个路径就能跑。如果你也有类似的日志分析场景不妨试试看。有问题欢迎在评论区交流或者告诉我你用什么方案处理过大的 CSV。