duckdb-behavioral duckdb-behavioral

CI E2E Tests License: MIT MSRV: 1.88

duckdb-behavioral

Behavioral analytics for DuckDB -- session analysis, conversion funnels, retention cohorts, and event sequence pattern matching, all inside your SQL queries.

duckdb-behavioral is a loadable DuckDB extension written in Rust that brings ClickHouse-style behavioral analytics functions to DuckDB. It ships eight battle-tested functions that cover the core patterns of user behavior analysis โ€” all six ClickHouse behavioral functions with source-verified semantics, plus two extensions โ€” and benchmark-validated performance at billion-row scale.

No external services, no data pipelines, no additional infrastructure. Load the extension, write SQL, get answers.


What Can You Do With This?

Session Analysis

Break a continuous stream of events into logical sessions based on inactivity gaps. Identify how many sessions a user has per day, how long each session lasts, and where sessions begin and end.

SELECT user_id, event_time,
  sessionize(event_time, INTERVAL '30 minutes') OVER (
    PARTITION BY user_id ORDER BY event_time
  ) as session_id
FROM events;

Conversion Funnels

Track how far users progress through a multi-step conversion funnel (page view, add to cart, checkout, purchase) within a time window. Identify exactly where users drop off.

SELECT user_id,
  window_funnel(INTERVAL '1 hour', event_time,
    event_type = 'page_view',
    event_type = 'add_to_cart',
    event_type = 'checkout',
    event_type = 'purchase'
  ) as furthest_step
FROM events
GROUP BY user_id;

Retention Cohorts

Measure whether users who appeared in a cohort (e.g., signed up in January) returned in subsequent periods. Build the classic retention triangle directly in SQL.

SELECT cohort_month,
  retention(
    activity_date = cohort_month,
    activity_date = cohort_month + INTERVAL '1 month',
    activity_date = cohort_month + INTERVAL '2 months',
    activity_date = cohort_month + INTERVAL '3 months'
  ) as retained
FROM user_activity
GROUP BY user_id, cohort_month;

Event Sequence Pattern Matching

Detect complex behavioral patterns using a mini-regex over event conditions. Find users who viewed a product, then purchased within one hour -- with any number of intervening events.

SELECT user_id,
  sequence_match('(?1).*(?t<=3600)(?2)', event_time,
    event_type = 'view',
    event_type = 'purchase'
  ) as converted_within_hour
FROM events
GROUP BY user_id;

User Journey / Flow Analysis

Discover what users do after a specific behavioral sequence. What page do users visit after navigating from Home to Product? The inner query computes one next page per user; the outer query counts users per page (without the inner GROUP BY user_id, all users' events would form one sequence).

SELECT next_page, COUNT(*) AS user_count
FROM (
  SELECT user_id,
    sequence_next_node('forward', 'first_match', event_time, page,
      page = 'Home', page = 'Home', page = 'Product') AS next_page
  FROM events
  GROUP BY user_id
)
GROUP BY next_page
ORDER BY user_count DESC;

Quick Installation

Community Extension

The extension is listed in the DuckDB Community Extensions repository:

INSTALL behavioral FROM community;
LOAD behavioral;

No build tools, compilation, or -unsigned flag required.

From Source

git clone --recurse-submodules https://github.com/tomtom215/duckdb-behavioral.git
cd duckdb-behavioral
make configure release   # cargo build --release + metadata footer

This produces build/release/behavioral.duckdb_extension. DuckDB only loads files ending in .duckdb_extension, so the raw target/release/libbehavioral.so (or .dylib) cannot be loaded directly. Locally built extensions are unsigned, so start DuckDB with -unsigned:

duckdb -unsigned -c "LOAD 'build/release/behavioral.duckdb_extension'; SELECT behavioral_version();"

For detailed installation instructions, troubleshooting, and a complete worked example, see the Getting Started guide.


Functions

Choosing the Right Function

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', 'primaryTextColor': '#1a1a1a', 'primaryBorderColor': '#333333', 'lineColor': '#333333', 'secondaryColor': '#f5f5f5', 'tertiaryColor': '#e0e0e0', 'textColor': '#1a1a1a'}}}%%
flowchart TD
    Q{What do you want<br/>to analyze?}
    Q -->|"Break events<br/>into sessions"| S["sessionize"]
    Q -->|"Did users come<br/>back over time?"| R["retention"]
    Q -->|"How far through<br/>a multi-step flow?"| WF["window_funnel"]
    WF -->|"When did each<br/>step happen?"| WFE["window_funnel_events"]
    Q -->|"Did a specific<br/>event pattern occur?"| SM{Need details?}
    SM -->|"Yes/No answer"| SEQ["sequence_match"]
    SM -->|"How many times?"| SC["sequence_count"]
    SM -->|"When did each<br/>step happen?"| SME["sequence_match_events"]
    Q -->|"What happened<br/>next/before?"| SNN["sequence_next_node"]

    style Q fill:#ffffff,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style S fill:#e8e8e8,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style R fill:#d9d9d9,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style WF fill:#f0f0f0,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style WFE fill:#f0f0f0,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style SM fill:#ffffff,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style SEQ fill:#e8e8e8,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style SC fill:#e8e8e8,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style SME fill:#e8e8e8,stroke:#333333,stroke-width:2px,color:#1a1a1a
    style SNN fill:#d9d9d9,stroke:#333333,stroke-width:2px,color:#1a1a1a

Eight aggregate functions plus one diagnostic scalar:

FunctionTypeReturnsDescription
sessionizeAggregate (used with OVER (ORDER BY ...))BIGINTAssigns session IDs based on inactivity gaps
retentionAggregateBOOLEAN[]Cohort retention analysis
window_funnelAggregateINTEGERConversion funnel step tracking
window_funnel_eventsAggregateLIST(TIMESTAMP)Timestamps of the best funnel chain
sequence_matchAggregateBOOLEANPattern matching over event sequences
sequence_countAggregateBIGINTCount non-overlapping pattern matches
sequence_match_eventsAggregateLIST(TIMESTAMP)Return matched condition timestamps
sequence_next_nodeAggregateVARCHARNext event value after pattern match
behavioral_versionScalarVARCHARVersion of the loaded extension

Condition limits: window_funnel and window_funnel_events accept 1 to 32 conditions; retention, sequence_match, sequence_count, and sequence_match_events accept 2 to 32; sequence_next_node accepts a base condition plus 1 to 32 event conditions (ClickHouse allows up to 64 for sequenceNextNode). See the ClickHouse Compatibility page for the full parity matrix.


Performance

The numbers below are Criterion.rs microbenchmarks of the Rust aggregate state (update/combine/finalize), not end-to-end SQL queries, with 95% confidence intervals. They are the PERF.md Session 15 headline numbers, recorded before v0.8.0 and not re-measured since.

FunctionScaleWall ClockThroughput
sessionize_update1 billion events1.20 s830 Melem/s
retention_combine100 million states274 ms365 Melem/s
window_funnel_finalize100 million events791 ms126 Melem/s
sequence_match100 million events1.05 s95 Melem/s
sequence_count100 million events1.18 s85 Melem/s
sequence_match_events100 million events1.07 s93 Melem/s
sequence_next_node10 million events546 ms18 Melem/s

Key design choices that enable this performance:

  • 16-byte Copy events with u32 bitmask conditions -- four events per cache line, zero heap allocation per event
  • O(1) combine for sessionize and retention via boundary tracking and bitmask OR
  • In-place combine for event-collecting functions -- O(N) amortized instead of O(N^2) from repeated allocation
  • Sequence fast paths -- common pattern shapes dispatch to specialized O(n) linear scans; every other pattern uses a feasibility pass plus a greedy walk, O(s ยท n log n), replacing a backtracking search that was quadratic per group
  • Presorted detection -- O(n) check skips O(n log n) sort when events already arrive in timestamp order

Full methodology, per-element cost analysis, and optimization history are documented in the Performance section.


Engineering Highlights

This project demonstrates depth across systems programming, database internals, algorithm design, performance engineering, and software quality practices. For a comprehensive technical overview, see the Engineering Overview.

AreaHighlights
Language & SafetyPure Rust core with unsafe confined to the FFI bridge (src/ffi/, 9 files). Aggregate callbacks wrapped by quack-rs aggregate_*_callback! macros, which turn a panic into a SQL error. Zero clippy warnings under pedantic, nursery, and cargo lint groups.
Testing Rigor547 unit tests, 28 in-process integration tests that LOAD the built extension, 78 sqllogictest directives (44 query + 34 statement) across 8 SQL test files run against the DuckDB CLI, 31 property-based tests (proptest), 88.4% mutation kill rate (cargo-mutants, measured on v0.4.x and not re-measured since).
PerformanceOptimization sessions recorded in PERF.md with before/after Criterion.rs measurements and 95% confidence intervals, including a 1-billion-event sessionize_update benchmark and five documented negative results.
Algorithm DesignCustom pattern engine with recursive descent parser, fast-path classification, and a feasibility-then-greedy matcher differentially tested against a backtracking reference. Bitmask-based retention with O(1) combine.
Database InternalsDuckDB C API integration via quack-rs SDK with safe builders, state management, and vector I/O. Variadic signatures registered as function sets (31 overloads for retention and the sequence_match/sequence_count/sequence_match_events family, 64 for window_funnel/window_funnel_events, 32 for sequence_next_node). Stable C API (v1.2.0): one binary loads into DuckDB 1.3.2 through 2.0.0. Correct combine semantics for segment tree windowing.
CI/CD14 CI jobs (incl. a DuckDB-WASM compile check), 4-platform release builds, SemVer validation, artifact attestation, MSRV verification.
Feature CompletenessAll six ClickHouse behavioral functions (source-verified semantics) plus sessionize and window_funnel_events: 6 combinable funnel modes, 32-condition support, time-constrained pattern syntax.

Documentation

SectionContents
Getting StartedInstallation, loading, troubleshooting, your first analysis
Function ReferenceDetailed docs for all 8 functions with examples
Use CasesFive complete real-world examples with sample data and queries
SQL Cookbook25+ practical SQL recipes for common analytics patterns
Quick ReferenceOne-page cheat sheet for all functions and patterns
FAQCommon questions about loading, patterns, modes, NULLs
Engineering OverviewTechnical depth, architecture, quality standards, domain significance
ArchitectureModule structure, design decisions, FFI bridge
PerformanceBenchmarks, algorithmic complexity, optimization history
ClickHouse CompatibilitySyntax mapping, semantic parity matrix
OperationsCI/CD, security and supply chain, benchmarking methodology
ContributingDevelopment setup, testing expectations, PR process

Requirements

  • DuckDB 1.3.2 or later (built against 2.0.0; stable C API, so one binary serves every release CI checks: 1.3.2, 1.4.4, 1.5.0, 1.5.6, 2.0.0)
  • Rust 1.88+ (MSRV) for building from source
  • Python 3 and make for make configure release (stamps the metadata footer)

Source Code

The source code is available at github.com/tomtom215/duckdb-behavioral.

License

MIT