Linear Regression & Trend Forecasting in SQL

Linear Regression & Trend Forecasting in SQL

PostgreSQL has built-in regression aggregates (REGR_SLOPE, REGR_INTERCEPT, CORR, REGR_R2) that fit a linear model entirely in SQL — no Python, no external tools. We use these to forecast energy demand and detect deviating trends.

-- Fit a linear trend to each meter and forecast the next 7 days
WITH daily_kwh AS (
    SELECT
        meter_id,
        building,
        read_at::DATE                                           AS day,
        SUM(kwh)                                               AS daily_kwh,
        EXTRACT(EPOCH FROM read_at::DATE)                      AS epoch_day
    FROM ts_energy_readings
    GROUP BY meter_id, building, read_at::DATE
),
regression AS (
    SELECT
        meter_id,
        building,
        REGR_SLOPE(daily_kwh, epoch_day)                       AS slope,
        REGR_INTERCEPT(daily_kwh, epoch_day)                   AS intercept,
        REGR_R2(daily_kwh, epoch_day)                          AS r_squared,
        CORR(daily_kwh, epoch_day)                             AS correlation,
        COUNT(*)                                               AS data_points,
        MAX(epoch_day)                                         AS last_epoch,
        MAX(day)                                               AS last_day,
        AVG(daily_kwh)                                         AS avg_kwh
    FROM daily_kwh
    GROUP BY meter_id, building
),
forecasts AS (
    SELECT
        r.meter_id,
        r.building,
        r.slope,
        r.intercept,
        ROUND(r.r_squared::numeric, 3)                         AS r_squared,
        ROUND(r.avg_kwh::numeric, 2)                           AS avg_daily_kwh,
        r.last_day,
        gs.future_day,
        ROUND((r.slope * EXTRACT(EPOCH FROM gs.future_day)
               + r.intercept)::numeric, 2)                     AS forecast_kwh,
        CASE
            WHEN ABS(r.slope) < 0.0001                        THEN 'Flat'
            WHEN r.slope > 0                                   THEN 'Increasing'
            ELSE                                                    'Decreasing'
        END                                                    AS trend_direction
    FROM regression r
    CROSS JOIN LATERAL generate_series(
        (r.last_day + 1)::TIMESTAMP,
        (r.last_day + 7)::TIMESTAMP,
        INTERVAL '1 day'
    ) AS gs(future_day)
)
SELECT *
FROM forecasts
ORDER BY meter_id, future_day;

Purchase this course to unlock the full lesson.

Sign up