
By Tiger Data Team
Updated at Jul 7, 2026
Time-series forecasting is the process of using historical time-indexed observations to estimate future values of a variable. It differs from time-series analysis, which examines patterns in past data, in that forecasting is specifically predictive and forward-looking. The methods range from a 7-day moving average in a single SQL query to a zero-shot foundation model trained on billions of time points.
Most time-series forecasting guides assume Python from line one. The imports come first, then the DataFrame setup, then statsmodels or Prophet. If your data lives in PostgreSQL, you're left figuring out the connection layer yourself.
This guide works the other way. It maps what's achievable directly in the database against what genuinely requires a Python layer, and explains the practical criteria for drawing that line. Because Tiger Data builds on PostgreSQL, there's a natural bias worth naming: the in-database examples use TimescaleDB hyperfunctions where they add value. But the SQL methods here also apply to standard PostgreSQL, and the guide includes cases where Python is clearly the right call.
Two myths come up repeatedly in developer communities. The first: you need Python for any real forecasting. The second: ARIMA is the default or standard model for time-series work. Both are wrong in ways that matter, and this guide addresses them directly.
For the conceptual breakdown of time-series components (trend, seasonality, noise decomposition), see the foundational overview. This article picks up where that one leaves off: implementation.
The framework used throughout: each method is classified as in-database (pure SQL), Python-required, or a reasonable approximation in SQL with Python as the cleaner alternative. The goal is deliberate choice over reflexive defaults.
Methods are classified below by what they require, not what's theoretically possible. A recursive CTE can approximate exponential smoothing, but whether it should depends on dataset size, refresh requirements, and team familiarity.
Method | SQL native | Python required | When to use |
Simple moving average | Yes | No | Trend smoothing, dashboards, alerting baselines |
Time-weighted average | Yes ( | No | Irregular timestamps, IoT sensor data |
Linear regression (trend extrapolation) | Yes ( | No | Short-horizon, near-linear trend projection |
Exponential smoothing (simple) | SQL via recursive CTE | No, but slow at scale | When recent data should outweigh older observations |
Holt-Winters (double/triple) | SQL approximation possible | Recommended for seasonal variants | Stable seasonal patterns; use Python for seasonal tuning |
ARIMA / SARIMA | No | Yes (statsmodels) | Strong autocorrelation structure, irregular seasonality |
Prophet | No | Yes | Human-driven seasonality, trend changepoints, holiday effects |
ML-based (LSTM, XGBoost) | No | Yes | Non-linear patterns, many input features |
Foundation models (Chronos-2, TimesFM, MOIRAI-2) | No | Yes | Zero-shot forecasting; emerging, not yet production-standard |
For most Tiger Data setups, PostgreSQL is the source of truth and query engine, Python is the model layer for complex forecasting, and results write back to Postgres for serving. That's not a limitation of the database stack; it's a sensible division of responsibilities. Postgres handles high-throughput ingestion, time-range queries, and result serving at scale. Python handles iterative model fitting and the statistical machinery that SQL was never designed for.
Myth 1: "You need Python for any real forecasting." Moving averages, time-weighted averages, and linear regression-based trend extrapolation are all production-viable in SQL for monitoring, alerting, and short-horizon projection. The threshold for "you need Python" is iterative model fitting and parameter optimization, not forecasting in general.
Myth 2: "ARIMA is the standard model." For short-horizon, trend-following use cases, simpler methods often match ARIMA's accuracy with far less complexity. The broader community has moved toward foundation models and tree-based ML for structured business data. ARIMA remains useful when autocorrelation structure is strong, but it stopped being a default years ago.
These methods are implementable entirely in SQL, without a Python process or external model server. All examples assume a table with a timestamp column and a numeric value column. time_bucket() requires TimescaleDB and time_weight() requires the TimescaleDB Toolkit extension (bundled on Tiger Cloud); regr_slope() and regr_intercept() are native PostgreSQL aggregates.
time_bucket()A simple moving average smooths short-term noise by averaging values over a rolling window. The result is a lag-adjusted trend signal, not a point forecast of a specific future value. For dashboards, alerting thresholds, and short-horizon projection on well-behaved series, this is often enough.
time_bucket() groups data into consistent hourly intervals; after averaging to one row per hour, a window over 168 of those hourly rows covers 7 days:
WITH hourly AS (
SELECT time_bucket('1 hour', recorded_at) AS bucket,
AVG(value) AS hourly_avg
FROM sensor_readings
GROUP BY bucket
)
SELECT
bucket,
AVG(hourly_avg) OVER (
ORDER BY bucket
ROWS BETWEEN 167 PRECEDING AND CURRENT ROW
) AS moving_avg_7d
FROM hourly
ORDER BY bucket;The limitation matters: standard SQL window functions treat rows equally regardless of timestamp gaps. If data is irregular (missing hours, variable reporting intervals), the window spans the wrong duration and produces incorrect averages. For irregular data, time_weight() is the correct tool.
For a deeper implementation guide including SMA variants and edge case handling, see moving averages in SQL.
Real sensor data often arrives with gaps or variable reporting intervals. A row-based window average double-counts dense periods and under-weights sparse ones. time_weight() weights each observation by the duration it represents, so a reading held for 10 minutes contributes proportionally more than one held for 30 seconds.
SELECT
time_bucket('1 hour', recorded_at) AS bucket,
average(time_weight('Linear', recorded_at, value)) AS time_weighted_avg
FROM sensor_readings
GROUP BY bucket
ORDER BY bucket;Use this for IoT sensor data, financial tick data, and any series where data arrival rate is not constant. It's a TimescaleDB Toolkit function with no direct equivalent in standard PostgreSQL.
regr_slope() and regr_intercept() are native PostgreSQL aggregate functions that fit a least-squares regression line over a time window. No extension required. The slope and intercept project the trend forward in a single query with no external dependencies.
Computing a regression-based 24-hour-ahead forecast from the past 7 days of hourly data:
SELECT
regr_slope(value, EXTRACT(EPOCH FROM recorded_at)) AS slope,
regr_intercept(value, EXTRACT(EPOCH FROM recorded_at)) AS intercept,
regr_slope(value, EXTRACT(EPOCH FROM recorded_at))
* EXTRACT(EPOCH FROM NOW() + INTERVAL '24 hours')
+ regr_intercept(value, EXTRACT(EPOCH FROM recorded_at)) AS forecast_24h
FROM sensor_readings
WHERE recorded_at > NOW() - INTERVAL '7 days';This approach assumes a linear trend, so it degrades quickly on seasonal or cyclical data and becomes unreliable beyond short forecast horizons. Use it when the trend is genuinely near-linear over the recent window. For longer horizons or non-linear patterns, the next tier of methods applies.
Exponential smoothing assigns decreasing weights to older observations, with a smoothing factor alpha (0 < alpha < 1). Higher alpha means more weight on recent data; lower alpha means more weight on history. It's computationally simple and performs well on trend-following series without seasonal components.
WITH ordered AS (
SELECT recorded_at, value,
row_number() OVER (ORDER BY recorded_at) AS rn
FROM sensor_readings
)
-- , then recurse over the row index (no ORDER BY / LIMIT in the recursion)
, ema AS (
SELECT recorded_at, value, value AS ema_value, rn
FROM ordered WHERE rn = 1
UNION ALL
SELECT o.recorded_at, o.value,
0.2 * o.value + 0.8 * e.ema_value, o.rn
FROM ordered o
JOIN ema e ON o.rn = e.rn + 1
)
SELECT recorded_at, value, ema_value FROM ema
ORDER BY recorded_at;Recursive CTEs in PostgreSQL process rows one at a time and slow down on large datasets. For production use at scale, pre-compute via continuous aggregates or move the EMA computation to a Python layer. That's the real boundary: when the recursive CTE becomes a bottleneck, you've crossed into Python territory.
Holt-Winters extends exponential smoothing with trend and seasonality components. A double-exponential version (Holt's linear method) is approximable in SQL using two-pass recursive CTEs, but the triple-exponential seasonal variant requires iterative optimization. At that point, Python's statsmodels is the right tool. The SQL approximation works as a prototype or low-volume implementation; production seasonal forecasting belongs in Python.
If your moving average or regression scans the full historical table on every query, performance degrades as data grows. A query that took 100ms against 30 days of data may take seconds against 18 months.
Continuous aggregates solve this by pre-computing and incrementally refreshing rolled-up statistics on a schedule. Forecasting queries hit the materialized view, not the raw hypertable.
CREATE MATERIALIZED VIEW hourly_sensor_averages
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', recorded_at) AS bucket,
AVG(value) AS avg_value,
COUNT(*) AS reading_count,
STDDEV(value) AS stddev_value
FROM sensor_readings
GROUP BY bucket
WITH NO DATA;
SELECT add_continuous_aggregate_policy('hourly_sensor_averages',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '5 minutes');Continuous aggregates refresh only changed time buckets, not the full dataset. A 5-minute refresh cycle on hourly buckets keeps your forecasting inputs within 5-10 minutes of current without a full-table scan on every query.
This is the practical answer to the batch vs. real-time tradeoff. Pre-aggregation work is incremental, so near-real-time forecasting inputs are tractable without sacrificing query performance. For implementation detail and refresh policy options, see continuous aggregates in TimescaleDB.
There's no penalty for using Python for forecasting. The point is to choose deliberately. Python is the right layer when the model requires iterative fitting, parameter optimization, or non-linear pattern capture that SQL was not designed to handle.
All methods in this section use PostgreSQL as the data source: connect via psycopg2 or SQLAlchemy, pull pre-aggregated data from the database, fit the model, and write results back to Postgres for serving.
ARIMA (Autoregressive Integrated Moving Average) models the relationship between an observation and its lagged values, applies differencing to achieve stationarity, and models residual error as a moving average. SARIMA adds seasonal terms. The model requires iterative parameter estimation (selecting p, d, q values) that is not tractable in SQL.
A minimal implementation pulling pre-aggregated hourly data from PostgreSQL and fitting an ARIMA model:
import psycopg2
import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
conn = psycopg2.connect("host=localhost dbname=mydb user=postgres")
df = pd.read_sql(
"SELECT bucket, avg_value FROM hourly_sensor_averages "
"WHERE bucket > NOW() - INTERVAL '90 days' "
"ORDER BY bucket",
conn
)
model = ARIMA(df['avg_value'], order=(2, 1, 2))
result = model.fit()
forecast = result.forecast(steps=24)The query pulls from the continuous aggregate view, not the raw hypertable. This keeps the data pull fast regardless of how much raw data has accumulated.
ARIMA tends to outperform simpler methods when autocorrelation structure is strong and the series is stationary or made stationary via differencing. For series with many external factors or non-linear patterns, tree-based ML often outperforms ARIMA on structured business data with less tuning overhead.
On stationarity: ARIMA assumes the statistical properties of the series (mean, variance, autocorrelation) do not change over time. Non-stationary series violate this assumption and produce unreliable forecasts. The Augmented Dickey-Fuller (ADF) test in Python's statsmodels.tsa.stattools is the standard check. The "I" component in ARIMA applies differencing to remove trends and achieve stationarity before fitting.
For deeper coverage of AR modeling concepts, see autoregressive time-series modeling.
Prophet (Meta open source) is designed for business time-series data with trend changepoints, seasonality, and known holiday or event effects. It handles missing data and outliers more gracefully than ARIMA, and requires less manual parameter selection. The trade-off is that it needs more history for reliable changepoint detection, typically at least one year.
A minimal Prophet implementation with a PostgreSQL data pull:
import psycopg2
import psycopg2
import pandas as pd
from prophet import Prophet
conn = psycopg2.connect("host=localhost dbname=mydb user=postgres")
df = pd.read_sql(
"SELECT bucket AS ds, avg_value AS y "
"FROM hourly_sensor_averages "
"WHERE bucket > NOW() - INTERVAL '365 days' "
"ORDER BY bucket",
conn
)
model = Prophet(yearly_seasonality=True, weekly_seasonality=True)
model.fit(df)
future = model.make_future_dataframe(periods=24, freq='H')
forecast = model.predict(future)Prophet makes sense for demand series, web traffic, sales, and other business metrics with clear weekly and seasonal patterns and identifiable event effects. For univariate series without human-driven seasonality, ARIMA or a moving average is often more appropriate and faster to run.
For IoT fleets with thousands of sensor series, or any use case requiring parallelized forecasting across many series, the Python ecosystem has purpose-built libraries. Nixtla's StatsForecast and NeuralForecast, Darts, and sktime all treat forecasting as a batch-parallelizable operation. They handle cross-learning across series, multiple input features, and batch inference at a scale that single-series ARIMA cannot match.
The architectural pattern stays the same: Tiger Data as the ingestion and query layer, Python as the forecasting engine, results written back to a forecasts table in PostgreSQL for downstream serving and alerting. What changes is the scale of the batch export and the parallelism in the Python layer.
For the Python-specific tutorial including data pre-processing and model assessment, see time-series analysis and forecasting with Python.
Time-series foundation models are large models pre-trained on diverse time-series datasets, ranging from hundreds of millions to billions of time points. They can forecast new series zero-shot, without per-series training. The analogy to LLMs for text is reasonable: instead of training on tokens, they train on numerical sequences and generalize to unseen domains. This is emerging technology, not yet production-standard for most teams, but worth understanding for where the field is moving.
Three models have established themselves as benchmarks in this space:
Chronos-2 (Amazon Science): Pre-trained on a large corpus of diverse time-series data producing probabilistic (quantile) forecasts. Extends beyond univariate to multivariate and covariate-informed forecasting in a single model, zero-shot via in-context learning. Available via HuggingFace. Practical for teams with short history per series or many distinct series to forecast.
TimesFM (Google Research): The original TimesFM was trained on roughly 100 billion time points from Google-internal and public sources. The current release, TimesFM 2.5 (200M parameters, 16k context), shows strong long-horizon performance and is available as an open-weights model.
MOIRAI-2 (Salesforce): Universal time-series forecasting model with strong zero-shot performance across diverse domains including energy, retail, and financial data. Available via HuggingFace.
For all three, the integration path is the same: a Python script pulls data from PostgreSQL, runs inference using the model's API, and returns a forecast. TimescaleDB handles the data pipeline; the foundation model handles the compute.
Worth watching on the in-database side: tspDB (MIT) is a PostgreSQL extension that builds a prediction index on top of a standard table, enabling in-database predictive queries - forecasting, imputation, and confidence bounds - through a prediction index created with create_pindex(). It's experimental and not production-ready as of June 2026, but it represents the research direction toward natively integrated forecasting without a Python layer.
Here's how the pieces wire together in production. The two-layer architecture below covers most SQL forecasting use cases.
Raw time-series data ingests into a hypertable. A continuous aggregate pre-computes hourly averages on a 5-minute refresh cycle. A retention policy drops raw data older than 90 days while preserving the aggregates. On top of that: a scheduled job (cron, Airflow, or another workflow orchestrator) pulls the pre-aggregated data via SQLAlchemy, fits ARIMA or Prophet, and writes forecast rows back to a forecasts table.
The schema for the forecasts table:
CREATE TABLE forecasts (
series_id TEXT NOT NULL,
forecast_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
horizon_hours INTEGER NOT NULL,
forecast_value DOUBLE PRECISION NOT NULL,
confidence_lower DOUBLE PRECISION,
confidence_upper DOUBLE PRECISION,
model_name TEXT,
PRIMARY KEY (series_id, forecast_at, horizon_hours)
);Writing forecasts back to Postgres matters for three reasons. First, downstream dashboards (Grafana, Metabase) query the forecasts table with standard SQL, no specialized tooling required. Second, alerting queries can join actual vs. forecast to detect deviation from expected trajectory. Third, the forecast audit trail stays in the same system as the source data, which simplifies debugging and model versioning.
For context on retention policies and tiered data resolution that feed the pre-aggregation layer, see time-series downsampling. For how to combine forecasting signals with anomaly alerts, see time-series anomaly detection.
Choose SQL-native moving averages if:
You need trend smoothing or rolling baselines for dashboards or alerting
Your forecast horizon is short (hours to a few days)
Your data is regular, or you can tolerate a simple row-based average
Speed and simplicity matter more than model precision
Choose SQL linear regression if:
The series has a near-linear trend over the recent window
You're extrapolating 24 to 72 hours ahead
You want a one-query implementation with no external dependencies
Choose SQL exponential smoothing if:
Recent observations should outweigh older ones
There is no seasonal component, or the seasonal period is handled elsewhere
Dataset is small enough that recursive CTE performance is acceptable
Choose Python (ARIMA / SARIMA) if:
Autocorrelation structure is strong and you need proper parameter selection
Seasonality is a factor and a Holt-Winters SQL approximation is not accurate enough
You need confidence intervals from a statistically rigorous model
Choose Python (Prophet) if:
Your data has human-driven seasonality (weekly, holiday, business cycle patterns)
Trend changepoints matter (sales, traffic, demand series with promotion events)
You have enough history for changepoint detection (typically 1+ year)
Choose Python with foundation models if:
You have many short series without enough per-series history for ARIMA or Prophet
You want zero-shot forecasting without per-series training
You're evaluating state-of-the-art options and can tolerate experimental tooling
In production, hybrid combinations are common: SQL moving averages for real-time dashboards running alongside ARIMA or Prophet in a nightly batch for medium-horizon forecasts. The two layers complement each other without conflict.
TimescaleDB runs on PostgreSQL. If you're already on Postgres and want to add time-series primitives (hypertables, continuous aggregates, time_weight()) to your forecasting stack, start a Tiger Cloud trial or explore the TimescaleDB docs to get started with self-hosted deployment.
Time-series forecasting is the process of using historical time-indexed observations to estimate future values of a variable. It differs from time-series analysis, which focuses on understanding patterns in past data; forecasting is specifically predictive and forward-looking. The main method families include statistical models (ARIMA, exponential smoothing), machine learning (tree-based models, LSTM), and emerging foundation models such as Chronos-2 and TimesFM.
The four main categories are: naive and statistical methods (moving average, exponential smoothing), classical statistical models (ARIMA, SARIMA, Holt-Winters), machine learning models (XGBoost on time-indexed features, LSTM), and foundation models (Chronos-2, TimesFM, MOIRAI-2). Simpler models often match or outperform complex ones on short-horizon, well-behaved series.
Yes. Moving averages, time-weighted averages, and linear regression are all implementable in standard SQL using window functions and aggregate functions such as regr_slope() and regr_intercept(). TimescaleDB extends this with time_bucket() for consistent bucketing and time_weight() for irregular timestamps. For models that require iterative fitting (ARIMA, Prophet, ML models), Python is the appropriate layer on top of PostgreSQL.
Exponential smoothing assigns decreasing weights to older observations and is computationally simple, making it a good baseline for trend-following series. ARIMA explicitly models autocorrelation structure and applies differencing to achieve stationarity. For short-horizon, trend-following series, exponential smoothing often performs comparably to ARIMA with less complexity. ARIMA tends to offer an advantage when autocorrelation patterns are strong and irregular.
ARIMA and related statistical models assume that the statistical properties of the series (mean, variance, and autocorrelation) do not change over time. Non-stationary series with trends or changing variance violate this assumption and produce unreliable forecasts. Differencing, the 'I' component in ARIMA, is the standard technique for removing trends and achieving stationarity before fitting the model.
Not categorically. Holt-Winters is simpler and interpretable, making it a strong baseline for series with stable seasonal patterns. SARIMA can model more complex seasonal structures and handles non-stationarity more explicitly. In practice, both should be benchmarked. For many short-horizon business forecasting problems, Holt-Winters performs comparably to SARIMA with less tuning overhead.
Prophet is designed for business time-series with human-driven seasonality (weekly patterns, holiday effects, promotional events) and trend changepoints. It handles missing data and outliers well. ARIMA is better suited to stationary series with strong autocorrelation structure where a formal statistical model with confidence intervals is required. Prophet generally needs at least one year of history for reliable changepoint detection.
Use a window function with AVG() over a ROWS BETWEEN N PRECEDING AND CURRENT ROW clause, combined with time_bucket() in TimescaleDB to group data into consistent time intervals. The result is a smoothed estimate of the current trend, useful for dashboards, anomaly detection baselines, and short-horizon projection when the trend is stable. For irregular timestamps, use time_weight() instead of a simple row-based window.
Foundation models such as Chronos-2, TimesFM, and MOIRAI-2 are large models pre-trained on diverse time-series datasets that can forecast new series zero-shot without per-series training. They use a Python API and connect to PostgreSQL data via standard database connectors. They are an emerging option showing strong benchmark performance, but are not yet production-standard for most teams.
External events (promotions, outages, weather, economic shocks) create structural breaks that purely statistical models miss. Prophet handles known events via explicit holiday and event regressors. ARIMA requires manual intervention analysis for structural breaks. The practical recommendation is to flag known events in your data and evaluate model performance separately for event and non-event periods before deploying a forecast.
Time-series analysis is retrospective: it identifies patterns, trends, seasonality, and anomalies in historical data. Time-series forecasting is prospective: it uses those patterns to estimate future values. In practice, analysis informs forecasting. You identify the trend and seasonal structure through analysis before selecting a forecasting model that can replicate that structure.
The standard pattern is to ingest raw time-series data into a hypertable, use a continuous aggregate to pre-compute hourly or daily averages on a refresh schedule, then run a scheduled Python job that pulls the pre-aggregated data, fits a forecasting model, and writes results back to a forecasts table in PostgreSQL. Downstream dashboards and alerting queries join the forecasts table against actuals using standard SQL.