Back to The Times of Claw

DuckDB for Sales Analytics: Pipeline Metrics Without a BI Tool

Run sales pipeline analytics with DuckDB: win rates, pipeline velocity, forecast accuracy, rep performance — all from SQL, no BI tool subscription needed.

Mark Rachapoom
Mark Rachapoom
·6 min read
DuckDB for Sales Analytics: Pipeline Metrics Without a BI Tool

DuckDB for Sales Analytics: Pipeline Metrics Without a BI Tool

Sales analytics doesn't require a $3,000/month BI tool. DuckDB gives you every pipeline metric that matters — win rates, stage velocity, rep performance, forecast accuracy, deal risk — in SQL against your own data.

This guide covers the queries that drive real sales decisions.

Schema Setup#

CREATE TABLE deals (
    deal_id VARCHAR PRIMARY KEY,
    company_id VARCHAR,
    owner_id VARCHAR,
    stage VARCHAR,
    value DECIMAL(12, 2),
    probability INTEGER,  -- 0-100
    created_at DATE,
    close_date DATE,
    closed_at DATE,
    close_reason VARCHAR,  -- 'won', 'lost', 'churned'
    lost_reason VARCHAR,
    source VARCHAR  -- 'outbound', 'inbound', 'referral', 'partner'
);
 
CREATE TABLE stage_history (
    deal_id VARCHAR,
    from_stage VARCHAR,
    to_stage VARCHAR,
    entered_at TIMESTAMP,
    exited_at TIMESTAMP
);
 
CREATE TABLE reps (
    rep_id VARCHAR PRIMARY KEY,
    name VARCHAR,
    team VARCHAR,
    quota DECIMAL(12, 2)
);

Pipeline Overview#

-- Current pipeline by stage
SELECT 
    stage,
    COUNT(*) AS deals,
    SUM(value) AS pipeline_value,
    AVG(value) AS avg_deal_size,
    AVG(DATEDIFF('day', created_at, CURRENT_DATE)) AS avg_age_days,
    SUM(value) OVER () AS total_pipeline,
    ROUND(SUM(value) * 100.0 / SUM(value) OVER (), 1) AS pct_of_pipeline
FROM deals
WHERE closed_at IS NULL
ORDER BY 
    CASE stage
        WHEN 'Lead' THEN 1
        WHEN 'Qualified' THEN 2
        WHEN 'Proposal' THEN 3
        WHEN 'Negotiation' THEN 4
        ELSE 5
    END;

Win Rate Analysis#

-- Win rate overall and by source
SELECT 
    source,
    COUNT(*) AS total_deals,
    COUNT(CASE WHEN close_reason = 'won' THEN 1 END) AS won,
    COUNT(CASE WHEN close_reason = 'lost' THEN 1 END) AS lost,
    ROUND(COUNT(CASE WHEN close_reason = 'won' THEN 1 END) * 100.0 / COUNT(*), 1) AS win_rate_pct,
    SUM(CASE WHEN close_reason = 'won' THEN value ELSE 0 END) AS revenue_won
FROM deals
WHERE closed_at IS NOT NULL
    AND closed_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY source
ORDER BY revenue_won DESC;
 
-- Win rate by stage it was lost in
SELECT 
    lost_reason,
    COUNT(*) AS deals_lost,
    SUM(value) AS value_lost,
    ROUND(AVG(DATEDIFF('day', created_at, closed_at)), 0) AS avg_days_in_cycle
FROM deals
WHERE close_reason = 'lost'
    AND closed_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY lost_reason
ORDER BY deals_lost DESC;

Sales Rep Performance#

-- Rep leaderboard: current quarter
SELECT 
    r.name AS rep,
    r.team,
    COUNT(CASE WHEN d.close_reason = 'won' THEN 1 END) AS deals_closed,
    SUM(CASE WHEN d.close_reason = 'won' THEN d.value ELSE 0 END) AS revenue,
    r.quota,
    ROUND(SUM(CASE WHEN d.close_reason = 'won' THEN d.value ELSE 0 END) * 100.0 / r.quota, 1) AS quota_attainment_pct,
    COUNT(CASE WHEN d.closed_at IS NULL THEN 1 END) AS open_deals,
    SUM(CASE WHEN d.closed_at IS NULL THEN d.value ELSE 0 END) AS open_pipeline
FROM reps r
LEFT JOIN deals d ON d.owner_id = r.rep_id
    AND (d.closed_at IS NULL OR (
        d.closed_at >= DATE_TRUNC('quarter', CURRENT_DATE)
        AND d.closed_at < DATE_TRUNC('quarter', CURRENT_DATE) + INTERVAL '3 months'
    ))
GROUP BY r.name, r.team, r.quota
ORDER BY revenue DESC;

Pipeline Velocity#

-- Average days in each stage
SELECT 
    from_stage AS stage,
    COUNT(*) AS transitions,
    ROUND(AVG(DATEDIFF('day', entered_at, exited_at)), 1) AS avg_days,
    ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY DATEDIFF('day', entered_at, exited_at)), 1) AS median_days,
    ROUND(PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY DATEDIFF('day', entered_at, exited_at)), 1) AS p90_days
FROM stage_history
WHERE exited_at IS NOT NULL
    AND entered_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY from_stage
ORDER BY avg_days DESC;
 
-- Deal cycle time from create to close
SELECT 
    ROUND(AVG(DATEDIFF('day', created_at, closed_at)), 0) AS avg_days_to_close,
    ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY DATEDIFF('day', created_at, closed_at)), 0) AS median_days,
    MIN(DATEDIFF('day', created_at, closed_at)) AS fastest,
    MAX(DATEDIFF('day', created_at, closed_at)) AS slowest
FROM deals
WHERE close_reason = 'won'
    AND closed_at >= CURRENT_DATE - INTERVAL '90 days';

Revenue Forecasting#

-- Bottom-up forecast: probability-weighted pipeline
SELECT 
    DATE_TRUNC('month', close_date) AS forecast_month,
    COUNT(*) AS deals_expected,
    SUM(value) AS total_pipeline,
    SUM(value * probability / 100.0) AS weighted_forecast,
    SUM(CASE WHEN probability >= 75 THEN value ELSE 0 END) AS commit_forecast
FROM deals
WHERE closed_at IS NULL
    AND close_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '3 months'
GROUP BY forecast_month
ORDER BY forecast_month;
 
-- Forecast accuracy: how accurate were last quarter's forecasts?
WITH quarterly_forecast AS (
    SELECT 
        DATE_TRUNC('month', closed_at) AS month,
        SUM(CASE WHEN close_reason = 'won' THEN value ELSE 0 END) AS actual_revenue
    FROM deals
    WHERE closed_at >= DATE_TRUNC('quarter', CURRENT_DATE) - INTERVAL '3 months'
        AND closed_at < DATE_TRUNC('quarter', CURRENT_DATE)
    GROUP BY month
)
SELECT 
    month,
    actual_revenue,
    LAG(actual_revenue) OVER (ORDER BY month) AS prior_month_actual,
    ROUND((actual_revenue - LAG(actual_revenue) OVER (ORDER BY month)) / 
          NULLIF(LAG(actual_revenue) OVER (ORDER BY month), 0) * 100, 1) AS mom_growth_pct
FROM quarterly_forecast
ORDER BY month;

Deal Risk Signals#

-- At-risk deals: stalled in a stage too long
SELECT 
    d.deal_id,
    d.company_id,
    d.stage,
    d.value,
    d.owner_id,
    DATEDIFF('day', sh.entered_at, CURRENT_TIMESTAMP) AS days_in_stage,
    sh.entered_at AS stage_entered_at
FROM deals d
JOIN stage_history sh ON d.deal_id = sh.deal_id AND sh.exited_at IS NULL
WHERE d.closed_at IS NULL
    AND (
        (d.stage = 'Qualified' AND DATEDIFF('day', sh.entered_at, CURRENT_TIMESTAMP) > 14)
        OR (d.stage = 'Proposal' AND DATEDIFF('day', sh.entered_at, CURRENT_TIMESTAMP) > 21)
        OR (d.stage = 'Negotiation' AND DATEDIFF('day', sh.entered_at, CURRENT_TIMESTAMP) > 30)
    )
ORDER BY days_in_stage DESC;

DenchClaw Sales Analytics#

DenchClaw is built on DuckDB — your pipeline data lives in the same database these queries run against. The v_deals PIVOT view gives you clean column names:

-- DenchClaw pipeline query
SELECT 
    "Stage",
    COUNT(*) AS deals,
    SUM("Deal Value") AS pipeline_value,
    AVG("Deal Value") AS avg_size
FROM v_deals
WHERE "Status" = 'Active'
GROUP BY "Stage"
ORDER BY pipeline_value DESC;

Ask the DenchClaw agent directly: "What's my pipeline by stage this month?" — it runs the SQL and returns a formatted report with charts.

Frequently Asked Questions#

How do I track stage-level conversion rates?#

Join the stage_history table and calculate the count of deals that moved from each stage to the next, divided by deals that entered that stage.

What's the right pipeline coverage ratio?#

Rule of thumb: 3-4x your quota in pipeline to hit your number. Calculate it as total pipeline value / quarterly quota.

How do I detect deal slippage?#

Compare close_date values over time. Deals that have moved their close date more than 30 days into the future are slipping.

Can I automate weekly pipeline reports?#

Yes — schedule a Python script that runs these DuckDB queries and emails the results, or use DenchClaw's scheduled agent to send pipeline summaries to Slack.

What's the best way to visualize pipeline funnel in DuckDB?#

Export the funnel query to JSON and render with Chart.js, or use the DenchClaw App Builder to create a live funnel dashboard.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA