用 DuckDB 把 CSV 日志分析压成一条命令:本地十分钟上手实践

举报
yd_238692410 发表于 2026/11/02 09:11:06 2026/11/02
【摘要】 DuckDB 单文件、无服务端,直接对 CSV/Parquet 跑 SQL。本文记录一次实际排查中的最小流程:显式声明列类型读日志、清洗落 Parquet、按小时聚合与 QUALIFY 去重、近似去重计数,再用 Python 出图,并列出它不适合的场景。

x背景:日志分析的尴尬时刻

排查一次线上问题,常见的流程是这样的:先把几台机器上的 nginx access log 拉下来,然后打开 pandas 读一遍,结果 2GB 的 CSV 让内存直接顶到 8GB 的机器上限;换 awk/grep 能跑,但想按小时聚合、按状态码分组、再去重统计独立 IP,一行行命令写下来就不太敢改了。

这类"一次性、中等规模、就在本地"的分析,其实有一个很好用的工具:DuckDB。它是一个进程内的列式分析数据库,单文件、无服务端、无依赖,直接对 CSV / Parquet / JSON 做 SQL 查询,而且不需要先把数据"导入"进库。

下面是我这次实际跑通的最小流程,全部在笔记本上完成。

一、先看清数据,不要急着写 SQL

DuckDB 的 CLI 是一个二进制文件,下载解压后直接运行:

duckdb mylog.db

进入交互式 SQL 之后,第一条永远是看数据长什么样:

DESCRIBE SELECT * FROM read_csv_auto('logs/2026-11-01/*.csv');

read_csv_auto 支持通配符,会把整个目录当成一张表。它靠采样猜测列类型和分隔符,这一步很省事,但也正是最容易踩坑的地方:

  • 如果某个字段前 1000 行全是数字、后面出现了 -,类型推断会失败,或者把该行当成坏行;
  • 如果时间戳是 2026-11-01T09:12:33+08:00 这种带偏移的格式,最好显式声明列类型,别指望自动识别。

稳妥的写法是显式给出列定义:

CREATE OR REPLACE VIEW raw AS
SELECT *
FROM read_csv('logs/2026-11-01/*.csv',
               header = true,
               delim = ' ',
               columns = {
                 'remote_addr': 'VARCHAR',
                 'ts':          'TIMESTAMP',
                 'method':      'VARCHAR',
                 'path':        'VARCHAR',
                 'status':      'INTEGER',
                 'bytes':       'BIGINT',
                 'ua':          'VARCHAR'
               },
               ignore_errors = true);

ignore_errors = true 会让解析失败的行被跳过而不是整体报错,代价是这些行会静默消失,所以跑完之后要对比一下行数:

SELECT count(*) FROM raw;

如果这里的行数明显小于 wc -l 的结果,说明列定义和数据格式没对上,先修好再做分析,否则后面的数字都是错的。

二、把清洗一次做掉,落成 Parquet

原始 CSV 只读一次就够了。把清洗结果物化成 Parquet,后续所有查询都会快一个量级,而且列式压缩后的体积通常只有 CSV 的三分之一到五分之一:

COPY (
  SELECT
    ts,
    remote_addr,
    method,
    path,
    status,
    bytes,
    regexp_extract(path, '^([^?]*)', 1) AS endpoint
  FROM raw
  WHERE status IS NOT NULL
) TO 'clean/logs_2026-11-01.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);

之后日常查询就只面对 Parquet:

SELECT * FROM read_parquet('clean/*.parquet');

read_parquet 支持 schema 演进,多天文件如果有新增列,加上 union_by_name = true 就能按列名合并,不会因为列顺序不同而错位。

三、几个真正省时间的 SQL 写法

按小时聚合,看流量和错误率曲线

SELECT
  date_trunc('hour', ts) AS hour,
  count(*)                                        AS reqs,
  sum(CASE WHEN status >= 500 THEN 1 ELSE 0 END)  AS err5xx,
  round(100.0 * sum(CASE WHEN status >= 500 THEN 1 ELSE 0 END) / count(*), 2) AS err_pct
FROM read_parquet('clean/*.parquet')
GROUP BY 1
ORDER BY 1;

取每个 IP 最新一条访问(去重用 QUALIFY)

不用嵌套子查询,DuckDB 支持 QUALIFY,写起来比 ROW_NUMBER() 外面再包一层干净很多:

SELECT remote_addr, ts, endpoint, status
FROM read_parquet('clean/*.parquet')
QUALIFY row_number() OVER (PARTITION BY remote_addr ORDER BY ts DESC) = 1;

Top N 接口 + 流量占比

WITH t AS (
  SELECT endpoint, count(*) AS reqs, sum(bytes) AS traffic
  FROM read_parquet('clean/*.parquet')
  GROUP BY 1
)
SELECT endpoint, reqs, traffic,
       round(100.0 * reqs / sum(reqs) OVER (), 2) AS pct
FROM t
ORDER BY reqs DESC
LIMIT 20;

窗口函数和聚合可以混在一条语句里,这比"先导出来再用 Excel 算占比"省事得多。

独立 IP 数不要用 count(distinct)

大基数上去重计数,count(distinct remote_addr) 很容易把内存吃满。改用近似算法:

SELECT approx_count_distinct(remote_addr) AS uv
FROM read_parquet('clean/*.parquet');

误差通常在 1%~2%,做趋势观察完全够用。

四、结果怎么出图

最省事的路径是让 DuckDB 把结果集直接交给 Python 绘图:

import duckdb
import matplotlib.pyplot as plt

con = duckdb.connect()
df = con.sql("""
    SELECT date_trunc('hour', ts) AS hour, count(*) AS reqs
    FROM read_parquet('clean/*.parquet')
    GROUP BY 1 ORDER BY 1
""").df()

df.plot(x="hour", y="reqs", figsize=(11, 3.5), legend=False)
plt.tight_layout()
plt.savefig("qps_by_hour.png", dpi=140)

con.sql(...).df() 走的是 Arrow 路径:结果是 Arrow 格式,转 pandas 时不会像 CSV 那样再解析一遍。这也是 DuckDB 和 pandas 配合最舒服的用法——重活交给 SQL,画图和后续处理交给 Python。

五、几个参数,能让它更快

PRAGMA threads = 8;          -- 默认用满物理核,机器忙时可以限流
PRAGMA memory_limit = '4GB'; -- 给系统留点余量,别让 OOM 杀掉编辑器

另外,如果是"只查最近一天"这种场景,把分区列做进目录名(dt=2026-11-01/part-0.parquet),再用 read_parquet('clean/dt=2026-11-01/*.parquet') 查询,就相当于做了一次分区裁剪,扫描量直接降下来。

六、它不适合做什么

  • 不适合高并发多写的业务库。DuckDB 面向分析场景,单写者、批量写入才是它的舒适区;
  • 不适合做在线服务的底层存储。每次查询都要重新读文件,没有连接池的概念;
  • CSV 自动推断不能当契约用。生产环境的数据落盘,最好在写入端就固定 schema,读端显式声明,别依赖采样猜测。

小结

这次排查我最后只留下两条命令:一条把当天日志清洗成 Parquet,一条导出按小时聚合的结果。给同事复现的时候,把这两条 SQL 发过去就能跑,不需要解释环境怎么装、依赖怎么配。

对"本地、一次性、几百 MB 到几十 GB"的日志分析,DuckDB 的性价比很高:它不需要你先把分析流程工程化,但也不像 shell 管道那样一旦要算占比和去重就变得难以维护。

如果你的机器上还留着几个常年不敢打开的 GB 级 CSV,可以先花十分钟试试 read_csv_auto 加 COPY ... TO ... (FORMAT PARQUET) 这两步,通常就是整个流程里收益最大的地方。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。