首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Parquet 存、DuckDB 查、Polars 转:数据处理的三道工序

Parquet 存、DuckDB 查、Polars 转:数据处理的三道工序

原创
作者头像
dsy
发布2026-08-24 15:47:49
发布2026-08-24 15:47:49
230
举报

上个月我接了个活儿。一个电商客户手里有几年的用户行为日志——浏览、下单、退款、点广告,全堆在 JSONL 文件里。

客户想做用户分层模型。我兴冲冲把日志往模型里一塞,模型直接翻脸。

原因很简单:模型要的是"这个用户买了几次、花了多少钱、活跃了几天"这种**用户级特征**。原始日志是"谁在哪一秒干了啥"的零散事件。两者中间差着一层。

那一刻我明白了:原始事件到模型特征之间,缺的不是模型,是一条管道。

我花了 5 天,用 DuckDB + Polars + Parquet 把这条管道搭了起来。今天先讲最底层的认知:三个工具各管一摊,谁也替代不了谁。

一、三个工具,三个角色

先给一句话定义:

  • **Parquet:负责存的**。一种面向分析场景的列式存储格式。
  • **DuckDB:负责查的**。一个嵌入式 SQL 引擎,不用起服务,对着文件就能查。
  • **Polars:负责转的**。一个高性能 DataFrame 工具,干清洗、转换、特征工程。

类比钉死:它们仨就像一家小饭馆的**仓库、点单台、后厨**。仓库管存货(Parquet),点单台管你要什么菜(SQL 查询),后厨管把食材做成能上桌的菜(Polars 清洗转换)。饭馆不是一个角色在扛,管道也一样。

Parquet 为什么不能用 CSV 凑合

CSV 是**按行**存:一行一条记录,读的时候整行搬进来。Parquet 是**按列**存:user_id 一列、amount 一列、timestamp 一列。

好处直接看表:

| 对比项 | CSV(行式) | Parquet(列式) |

| ---- | --------- | ----------- |

| 读取方式 | 整行读 | 只读需要的列 |

| 压缩 | 一般 | 更好 |

| 类型 | 全是字符串 | 保留真实类型 |

| 分析查询 | 慢 | 快 |

| 重复解析 | 每次都要解析字符串 | 不用 |

如果模型只要 user\_idamountcountry 三列,CSV 也得把 emailphonebrowser 整行搬进来。Parquet 直接跳过这些列,拎你要的走。

这就是列式存储的价值:**用多少,读多少。**

DuckDB:不用开机的 SQL

传统数据库要先起服务、建连接、建表。DuckDB 不用,直接在 Python 里跑:

代码语言:python
复制
import duckdb



connection = duckdb.connect()

它最擅长的,是直接对着 Parquet 文件写 SQL:

代码语言:python
复制
result = connection.execute("""

    SELECT user\_id, amount

    FROM read\_parquet('data/bronze/events.parquet')

    WHERE event = 'purchase'

      AND amount > 0

""").fetchall()

注意这里没有"先把 Parquet 读成 DataFrame"。是 DuckDB 直接查文件。它像是图书馆里那个不用你办卡、对着书架就能报出答案的管理员。

Polars:数据清洗的流水线

Polars 干的是脏活:类型转换、日期处理、按用户聚合。代码是表达式风格,不是逐行 for 循环。

代码语言:python
复制
import polars as pl



events = pl.read\_parquet("data/bronze/events.parquet")



cleaned = events.with\_columns(

    pl.col("amount").cast(pl.Float64),

    pl.col("timestamp").str.to\_datetime(),

)

这句的意思是:把 amount 转成浮点数,timestamp 转成真正的日期类型。一行写完,Polars 在底层并行处理,比你手写循环快得多。

二、JSON 和 JSONL,别再混了

这个项目踩的第一个概念坑,就是这俩。

**JSON** 整个文件必须是一个合法结构,通常是个数组:

代码语言:json
复制
[

  {"user\_id": 1001, "event": "purchase"},

  {"user\_id": 1002, "event": "page\_view"}

]

**JSONL**(也叫 NDJSON)每行一个独立 JSON:

代码语言:jsonl
复制
{"user\_id": 1001, "event": "purchase"}

{"user\_id": 1002, "event": "page\_view"}

类比:JSON 像一整本装订好的书,你得捧着整本读;JSONL 像收据打印机吐出来的纸条,一件一件往下接。

为什么日志用 JSONL?因为它:

  • 一行就是一条事件,新事件直接追加;
  • 可以逐行读,不用整文件加载进内存;
  • 某一行坏了,定位也快。

电商日志是持续产生的,JSONL 正好对得上这个节奏。

三、目录怎么分层

我按"数据成熟度"建了四层目录,灵感来自数据湖的 bronze / silver / gold(铜 / 银 / 金)分层:

| 目录 | 存什么 | 类比 |

| ------------- | ------------- | ------- |

| data/raw | 原始日志,绝不改 | 刚挖出来的矿石 |

| data/bronze | 统一转成的 Parquet | 粗炼后的锭 |

| data/silver | 清洗后的业务明细 | 精炼金属 |

| data/gold | 用户级模型特征 | 成品零件 |

第 1 天我没急着写业务,先把环境跑通。用 uv 管依赖,不用系统 Python:

代码语言:python
复制
import duckdb

import polars as pl

import pyarrow

import pydantic





def main() -> None:

    print("DuckDB:", duckdb.\_\_version\_\_)

    print("Polars:", pl.\_\_version\_\_)

    print("PyArrow:", pyarrow.\_\_version\_\_)

    print("Pydantic:", pydantic.\_\_version\_\_)





if \_\_name\_\_ == "\_\_main\_\_":

    main()

运行就一句:

代码语言:bash
复制
uv run python main.py

这条没业务逻辑的脚本,价值在确认三件事:依赖装好了、uv 能跑、四个库都能导入。地基稳了,后面才敢往上盖。

心得

5 天下来我最深的体会一句话:

**Parquet、DuckDB、Polars 不是谁替代谁,而是管道里三个分工明确的工位。**

存归存、查归查、转归转。以前我图省事,什么都往 Pandas + CSV 里塞,结果文件越攒越大、查询越来越慢。分清楚"存储、查询、转换"三件事,代码反而清爽了。

下篇我打算写 Bronze 层怎么把 JSONL 落进 Parquet,以及 DuckDB 怎么在文件上直接筛出购买事件。你平时处理数据,是 Pandas 一把梭,还是会像这样把存储、查询、转换拆开?评论区聊聊你踩过的坑。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、三个工具,三个角色
    • Parquet 为什么不能用 CSV 凑合
    • DuckDB:不用开机的 SQL
    • Polars:数据清洗的流水线
  • 二、JSON 和 JSONL,别再混了
  • 三、目录怎么分层
  • 心得
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档