DuckDB + Python:嵌入式 OLAP 数据库的轻量分析实战

文章目录

    • [一、DuckDB 的设计哲学:SQLite 的接口 + PostgreSQL 的性能](#一、DuckDB 的设计哲学:SQLite 的接口 + PostgreSQL 的性能)
    • [二、零配置数据查询:直接对文件写 SQL](#二、零配置数据查询:直接对文件写 SQL)
      • [多文件 Glob 联合查询](#多文件 Glob 联合查询)
    • [三、与 Python 生态的无缝集成](#三、与 Python 生态的无缝集成)
    • [四、特色 SQL:QUALIFY、ASOF JOIN 与 PIVOT](#四、特色 SQL:QUALIFY、ASOF JOIN 与 PIVOT)
      • QUALIFY:窗口函数过滤
      • [ASOF JOIN:时序不等值最近匹配](#ASOF JOIN:时序不等值最近匹配)
      • [PIVOT / UNPIVOT](#PIVOT / UNPIVOT)
    • [五、与 Pandas/Polars 的分工策略](#五、与 Pandas/Polars 的分工策略)
    • [六、实战:CSDN 用户行为分析](#六、实战:CSDN 用户行为分析)
    • 小结

数据分析领域存在一个"中间地带"------数据量超出 Excel 的 100 万行上限,但又不值得为了一次性分析去部署 PostgreSQL 或 ClickHouse 集群。Pandas 勉强能加载 500 万行,内存已经开始报警,GROUP BY 聚合耗时动辄数十秒。DuckDB 精准地填补了这个空白:它是零配置、零服务器的嵌入式 OLAP 引擎,直接对 CSV/Parquet/JSON 文件执行标准 SQL,不需要 CREATE TABLE,不需要 import,不需要写 ETL。


一、DuckDB 的设计哲学:SQLite 的接口 + PostgreSQL 的性能

DuckDB 与常见数据处理工具的本质区别在于定位:

工具 执行引擎 部署模式 典型数据量 强项
SQLite 行式 OLTP 嵌入式 < 100 万行 事务、CRUD、单机应用
Pandas Python 内存 库 < 500 万行 灵活的数据变形
DuckDB 列式 OLAP 嵌入式 100 万 ~ 10 亿行 SQL 分析、零配置
PostgreSQL 行式 OLTP 服务器 1000 万 ~ 10 亿行 OLTP + OLAP 混合
ClickHouse 列式 OLAP 服务器 10 亿+ 行 极致分析性能

DuckDB 的核心设计决策:

  • 列式存储 + 向量化执行 :分析型查询(SUM、GROUP BY、窗口函数)天然适合列式,向量化执行将一列数据打包为固定大小的向量,批量处理数千行,充分利用 CPU 缓存和 SIMD 指令。
  • 嵌入式无服务器 :整个数据库就是一个 .db 文件或 :memory: 模式,不需要守护进程、端口、认证。这个设计让 DuckDB 成为 Jupyter Notebook 和 Python 脚本场景中极有吸引力的选择。
  • PostgreSQL 兼容 SQL + 自研增强 :QUALIFY(窗口函数过滤)、ASOF JOIN(时序不等值连接)、PIVOT/UNPIVOT 等特性在 PostgreSQL 中需要多层子查询,DuckDB 将它们提升为一级语法。

二、零配置数据查询:直接对文件写 SQL

Pandas 分析 CSV 的标准流程是 read_csv → clean → query,每一步都在分配内存。DuckDB 将三步合并为一步:

python 复制代码
import duckdb

# 直接查询 CSV 文件------不需要先加载为 DataFrame
result = duckdb.sql("""
    SELECT category,
           COUNT(*) AS order_count,
           SUM(amount) AS total_revenue,
           AVG(amount) AS avg_order_value
    FROM 'orders_10m.csv'
    WHERE status = 'completed'
    GROUP BY category
    ORDER BY total_revenue DESC
    LIMIT 10
""")
df = result.df()  # 返回 Pandas DataFrame
pl_df = result.pl()  # 返回 Polars DataFrame(均为 Apache Arrow 零拷贝)

df() 和 pl() 方法的底层机制值得关注:DuckDB 的数据以 Apache Arrow 格式在内存中组织,调用 .df() 或 .pl() 时,数据通过 Arrow C Data Interface 直接"转移"到 Pandas/Polars 的内存空间,整个过程零拷贝。这比传统的"查询 → 序列化为中间格式 → 反序列化"路径快了数倍。

多文件 Glob 联合查询

python 复制代码
result = duckdb.sql("""
    SELECT strftime(order_date, '%Y-%m') AS month,
           category,
           SUM(amount) AS revenue
    FROM 'sales_2024_*.csv'   -- 自动匹配所有 sales_2024_01.csv, _02.csv, ...
    GROUP BY month, category
    ORDER BY month, revenue DESC
""")

FROM 'sales_2024_*.csv' 的 glob 模式是 DuckDB 的标志性特性------它自动扫描匹配的所有文件,在逻辑上合并为一张虚拟表。不需要手动 pandas.concat([pd.read_csv(f) for f in files]),不需要担心内存溢出------DuckDB 按需读取,只将查询需要的列和行加载到内存。


三、与 Python 生态的无缝集成

#mermaid-svg-Qd3w4bR8eTp9W1xx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Qd3w4bR8eTp9W1xx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Qd3w4bR8eTp9W1xx .error-icon{fill:#552222;}#mermaid-svg-Qd3w4bR8eTp9W1xx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Qd3w4bR8eTp9W1xx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Qd3w4bR8eTp9W1xx .marker.cross{stroke:#333333;}#mermaid-svg-Qd3w4bR8eTp9W1xx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Qd3w4bR8eTp9W1xx p{margin:0;}#mermaid-svg-Qd3w4bR8eTp9W1xx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Qd3w4bR8eTp9W1xx .cluster-label text{fill:#333;}#mermaid-svg-Qd3w4bR8eTp9W1xx .cluster-label span{color:#333;}#mermaid-svg-Qd3w4bR8eTp9W1xx .cluster-label span p{background-color:transparent;}#mermaid-svg-Qd3w4bR8eTp9W1xx .label text,#mermaid-svg-Qd3w4bR8eTp9W1xx span{fill:#333;color:#333;}#mermaid-svg-Qd3w4bR8eTp9W1xx .node rect,#mermaid-svg-Qd3w4bR8eTp9W1xx .node circle,#mermaid-svg-Qd3w4bR8eTp9W1xx .node ellipse,#mermaid-svg-Qd3w4bR8eTp9W1xx .node polygon,#mermaid-svg-Qd3w4bR8eTp9W1xx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Qd3w4bR8eTp9W1xx .rough-node .label text,#mermaid-svg-Qd3w4bR8eTp9W1xx .node .label text,#mermaid-svg-Qd3w4bR8eTp9W1xx .image-shape .label,#mermaid-svg-Qd3w4bR8eTp9W1xx .icon-shape .label{text-anchor:middle;}#mermaid-svg-Qd3w4bR8eTp9W1xx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Qd3w4bR8eTp9W1xx .rough-node .label,#mermaid-svg-Qd3w4bR8eTp9W1xx .node .label,#mermaid-svg-Qd3w4bR8eTp9W1xx .image-shape .label,#mermaid-svg-Qd3w4bR8eTp9W1xx .icon-shape .label{text-align:center;}#mermaid-svg-Qd3w4bR8eTp9W1xx .node.clickable{cursor:pointer;}#mermaid-svg-Qd3w4bR8eTp9W1xx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Qd3w4bR8eTp9W1xx .arrowheadPath{fill:#333333;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Qd3w4bR8eTp9W1xx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qd3w4bR8eTp9W1xx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Qd3w4bR8eTp9W1xx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qd3w4bR8eTp9W1xx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Qd3w4bR8eTp9W1xx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Qd3w4bR8eTp9W1xx .cluster text{fill:#333;}#mermaid-svg-Qd3w4bR8eTp9W1xx .cluster span{color:#333;}#mermaid-svg-Qd3w4bR8eTp9W1xx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Qd3w4bR8eTp9W1xx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Qd3w4bR8eTp9W1xx rect.text{fill:none;stroke-width:0;}#mermaid-svg-Qd3w4bR8eTp9W1xx .icon-shape,#mermaid-svg-Qd3w4bR8eTp9W1xx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Qd3w4bR8eTp9W1xx .icon-shape p,#mermaid-svg-Qd3w4bR8eTp9W1xx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Qd3w4bR8eTp9W1xx .icon-shape .label rect,#mermaid-svg-Qd3w4bR8eTp9W1xx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Qd3w4bR8eTp9W1xx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Qd3w4bR8eTp9W1xx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Qd3w4bR8eTp9W1xx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 处理层
查询层
SELECT * FROM 'file.csv'
.df() 零拷贝
.pl() 零拷贝
.arrow() 零拷贝
duckdb.sql(..., df)
duckdb.sql(..., pl_df)
CSV / Parquet / JSON

文件
DuckDB
Pandas DataFrame
Polars DataFrame
PyArrow Table

python 复制代码
# 方向一:从文件直接查询(最简路径)
top_users = duckdb.sql("""
    SELECT user_id, COUNT(*) AS visits
    FROM 'click_logs/*.csv'
    GROUP BY user_id
    ORDER BY visits DESC LIMIT 100
""").df()

# 方向二:以 Pandas DataFrame 作为 DuckDB 的"表"
import pandas as pd
user_df = pd.read_csv("users.csv")
enriched = duckdb.sql("""
    SELECT u.name, o.total_spent
    FROM user_df AS u
    JOIN 'orders.csv' AS o ON u.id = o.user_id
    WHERE o.total_spent > 1000
""").df()

# 方向三:DuckDB 数据库持久化
con = duckdb.connect("analytics.db")
con.execute("CREATE TABLE daily_stats AS SELECT * FROM 'orders.csv'")
con.execute("INSERT INTO daily_stats SELECT * FROM 'orders_new.csv'")
con.close()

四、特色 SQL:QUALIFY、ASOF JOIN 与 PIVOT

QUALIFY:窗口函数过滤

传统 SQL 中,对窗口函数结果做过滤需要套一层子查询:

sql 复制代码
-- PostgreSQL:需要子查询
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn
    FROM orders
) sub WHERE rn = 1;

-- DuckDB:QUALIFY 直接过滤窗口函数
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn
FROM orders
QUALIFY rn = 1;

在用户行为分析中"获取每个用户的最新一次下单"是高频需求,QUALIFY 将原本的两层嵌套压成一层,代码量减半且 SQL 优化器有更多优化空间。

ASOF JOIN:时序不等值最近匹配

金融场景中的经典需求:将每笔交易匹配到其发生时刻之前最近的一次报价。

sql 复制代码
SELECT t.trade_id, t.timestamp, t.price AS trade_price,
       q.price AS quote_price, q.timestamp AS quote_time
FROM trades AS t
ASOF JOIN quotes AS q
  ON t.symbol = q.symbol AND t.timestamp >= q.timestamp;

Pandas 需要用 pd.merge_asof,Polars 需要显式调用 join_asof(),DuckDB 直接将其作为 SQL 的 JOIN 类型------对习惯 SQL 的分析师更具可读性。

PIVOT / UNPIVOT

sql 复制代码
-- 行转列:每月各品类的销售额
PIVOT orders
ON MONTH(order_date)
USING SUM(amount)
GROUP BY category;

五、与 Pandas/Polars 的分工策略

三种工具的分工不是竞争而是互补:
#mermaid-svg-ZpOSNxjNEQvkQNIM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZpOSNxjNEQvkQNIM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZpOSNxjNEQvkQNIM .error-icon{fill:#552222;}#mermaid-svg-ZpOSNxjNEQvkQNIM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZpOSNxjNEQvkQNIM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZpOSNxjNEQvkQNIM .marker.cross{stroke:#333333;}#mermaid-svg-ZpOSNxjNEQvkQNIM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZpOSNxjNEQvkQNIM p{margin:0;}#mermaid-svg-ZpOSNxjNEQvkQNIM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZpOSNxjNEQvkQNIM .cluster-label text{fill:#333;}#mermaid-svg-ZpOSNxjNEQvkQNIM .cluster-label span{color:#333;}#mermaid-svg-ZpOSNxjNEQvkQNIM .cluster-label span p{background-color:transparent;}#mermaid-svg-ZpOSNxjNEQvkQNIM .label text,#mermaid-svg-ZpOSNxjNEQvkQNIM span{fill:#333;color:#333;}#mermaid-svg-ZpOSNxjNEQvkQNIM .node rect,#mermaid-svg-ZpOSNxjNEQvkQNIM .node circle,#mermaid-svg-ZpOSNxjNEQvkQNIM .node ellipse,#mermaid-svg-ZpOSNxjNEQvkQNIM .node polygon,#mermaid-svg-ZpOSNxjNEQvkQNIM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZpOSNxjNEQvkQNIM .rough-node .label text,#mermaid-svg-ZpOSNxjNEQvkQNIM .node .label text,#mermaid-svg-ZpOSNxjNEQvkQNIM .image-shape .label,#mermaid-svg-ZpOSNxjNEQvkQNIM .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZpOSNxjNEQvkQNIM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZpOSNxjNEQvkQNIM .rough-node .label,#mermaid-svg-ZpOSNxjNEQvkQNIM .node .label,#mermaid-svg-ZpOSNxjNEQvkQNIM .image-shape .label,#mermaid-svg-ZpOSNxjNEQvkQNIM .icon-shape .label{text-align:center;}#mermaid-svg-ZpOSNxjNEQvkQNIM .node.clickable{cursor:pointer;}#mermaid-svg-ZpOSNxjNEQvkQNIM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZpOSNxjNEQvkQNIM .arrowheadPath{fill:#333333;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZpOSNxjNEQvkQNIM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZpOSNxjNEQvkQNIM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZpOSNxjNEQvkQNIM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZpOSNxjNEQvkQNIM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZpOSNxjNEQvkQNIM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZpOSNxjNEQvkQNIM .cluster text{fill:#333;}#mermaid-svg-ZpOSNxjNEQvkQNIM .cluster span{color:#333;}#mermaid-svg-ZpOSNxjNEQvkQNIM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZpOSNxjNEQvkQNIM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZpOSNxjNEQvkQNIM rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZpOSNxjNEQvkQNIM .icon-shape,#mermaid-svg-ZpOSNxjNEQvkQNIM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZpOSNxjNEQvkQNIM .icon-shape p,#mermaid-svg-ZpOSNxjNEQvkQNIM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZpOSNxjNEQvkQNIM .icon-shape .label rect,#mermaid-svg-ZpOSNxjNEQvkQNIM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZpOSNxjNEQvkQNIM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZpOSNxjNEQvkQNIM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZpOSNxjNEQvkQNIM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Polars Lazy

复杂变形/流式处理
DuckDB

复杂 SQL/窗口/JOIN
Pandas/Ploty

可视化
原始数据文件
ETL 清洗阶段
干净数据
分析查询阶段
洞察结果
图表/报告

Polars 擅长 ETL 阶段的灵活数据变形(类型转换、缺失值填充、多源拼接),惰性求值和流式处理在处理大文件时优势显著。

DuckDB 擅长分析阶段的复杂 SQL(多表 JOIN、窗口函数、子查询),SQL 的表达能力在分析场景中比 Python API 更简洁。

Pandas/Plotly 擅长最终输出阶段的可视化和报告生成。

实际工作中一个常见的高效组合是:Polars 完成数据清洗 → 输出为 Parquet → DuckDB 执行分析 SQL → 结果通过 .df() 转到 Pandas → Matplotlib/Seaborn 出图。


六、实战:CSDN 用户行为分析

场景:12 个 CSV 文件(2024 年各月的点击、阅读、收藏、评论数据),用 DuckDB 完成从文件读取到漏斗分析的完整流程。

python 复制代码
import duckdb

con = duckdb.connect()

# 12 个 CSV 文件一键联合查询
user_actions = con.sql("""
    WITH all_events AS (
        SELECT user_id, event_type, article_id, event_time,
               strftime(event_time, '%Y-%m') AS month
        FROM 'user_behavior_2024_*.csv'
    ),
    user_profile AS (
        SELECT user_id,
               COUNT(*) AS total_events,
               COUNT(DISTINCT event_type) AS event_types_count,
               COUNT(DISTINCT article_id) AS unique_articles,
               COUNT(DISTINCT month) AS active_months,
               MAX(event_time) AS last_active,
               -- QUALIFY 替代子查询:找出每个用户最频繁的事件类型
               FIRST(event_type ORDER BY COUNT(*) DESC) AS top_event_type
        FROM all_events
        GROUP BY user_id
    ),
    funnel AS (
        SELECT
            COUNT(DISTINCT user_id) AS total_users,
            COUNT(DISTINCT CASE WHEN event_type = 'click'   THEN user_id END) AS clicked,
            COUNT(DISTINCT CASE WHEN event_type = 'read'    THEN user_id END) AS readed,
            COUNT(DISTINCT CASE WHEN event_type = 'collect' THEN user_id END) AS collected,
            COUNT(DISTINCT CASE WHEN event_type = 'comment' THEN user_id END) AS commented
        FROM all_events
    )
    SELECT * FROM funnel
""").df()

print(funnel)
# total_users  clicked  readed  collected  commented
#     152340   148200  128450     45320      12890

关键收益:

  • 12 个文件通过 FROM 'user_behavior_2024_*.csv' 自动合并,零代码。
  • QUALIFY 替代了窗口函数 + 子查询的两层嵌套。
  • 漏斗分析的 COUNT(DISTINCT CASE WHEN...) 对于 Pandas 来说需要多次 groupby + merge,DuckDB 一条 SQL 完成。
  • 数据量在千万行级别时,DuckDB 的查询延迟为百毫秒级,Pandas 同类操作需要数十秒。

小结

DuckDB 不是要去替代任何已有的工具,而是在"需要一个 SQL 分析引擎但不想部署数据库"的场景中给出了一个极低摩擦的选项。它的核心优势用三个关键词概括:零配置(文件即表)、列式高效(向量化执行)、零拷贝集成(Arrow 直通 Pandas/Polars)。在 Jupyter Notebook 中分析数百 MB 到数 GB 的结构化数据时,DuckDB 是目前性价比最高的方案。

此前专栏关于 Pandas 工程化分析、Polars 高性能数据处理以及数据管道工程化的文章,与本文共同构成了从数据清洗到分析查询的完整工具链。如果本文对数据分析工作流有所启发,欢迎点赞、收藏与关注。

相关推荐
小小程序猴14 分钟前
用工程视角建模企业AI培训周期:三段账本估算器(Python实现)
开发语言·人工智能·python
太理摆烂哥10 分钟前
MySQL表的操作
数据库
lbb 小魔仙12 分钟前
时序数据库是什么?2026 年五款工业时序数据库横评与场景解析
数据库·时序数据库
十二同学啊15 分钟前
Python FastAPI 项目使用 Docker 部署并映射端口(完整教程)
python·docker·fastapi
零基础12316 分钟前
权限管理怎么做?从“资格”与“空间”双维度设计一套可落地的权限体系
java·服务器·数据库
不爱吃鱼的蔓热莓18 分钟前
Autosar arxml异常字符串处理
python
FBI HackerHarry浩19 分钟前
Pytorch中 CPU tensor vs GPU tensor
人工智能·pytorch·python·深度学习
wewowilling20 分钟前
工作流 .... coze平台--AI--Agent--RAG--Function Calling
python·低代码·agent·零代码·工作流·coze
小草cys21 分钟前
图片一键转全矢量可编辑PPT——纯Python实现,零依赖,离线可用
人工智能·python·powerpoint
开开心心_Every29 分钟前
文件查重软件批量删除重复文件释放空间
java·开发语言·c#·计算机外设·ocr·excel·perl