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 的核心设计决策:

  • 列式存储 + 向量化执行 :分析型查询(SUMGROUP 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 高性能数据处理以及数据管道工程化的文章,与本文共同构成了从数据清洗到分析查询的完整工具链。如果本文对数据分析工作流有所启发,欢迎点赞、收藏与关注。

相关推荐
点灯小铭1 小时前
基于51单片机的LED点阵汉字显示系统设计
数据库·单片机·嵌入式硬件·毕业设计·51单片机·课程设计·期末大作业
无限进步_1 小时前
【Linux】进程状态、僵尸与孤儿、进程调度
linux·运维·服务器·开发语言·数据结构·算法
Nturmoils1 小时前
从 MySQL 到 KingbaseES:Database、Schema、User 一次讲透
数据库·后端
我是一颗柠檬1 小时前
【Redis】Redis面试高频考点汇总Day15(2026年)
数据库·redis·缓存·面试
爱上纯净的蓝天1 小时前
30 分钟上手 AtomCode:用它写一个 Python 批量整理文件/改名/生成报告小工具(新手教程)
python·开源·自动化脚本·atomcode·ai 编码助手
郝学胜-神的一滴1 小时前
力扣 662 :二叉树最大宽度
java·数据结构·c++·python·算法·leetcode·职场和发展
仙俊红1 小时前
反射到底解决什么问题?
java·开发语言
2301_764441331 小时前
基于Stackelberg博弈的分散式库存模型
python·算法·数学建模
大阳1231 小时前
ARM.9(RGBLCD,PWM)
c语言·开发语言·汇编·单片机·嵌入式硬件·pwm·rgblcd