Window functions in SQLite are a surprising way to perform complex analytical queries directly on your data without needing a dedicated data warehouse or specialized analytics database.

Let’s see this in action. Imagine you have a table sales with product_id, sale_date, and amount. You want to see each sale, but also the running total of sales for that product up to that date.

CREATE TABLE sales (
    product_id INTEGER,
    sale_date DATE,
    amount REAL
);

INSERT INTO sales (product_id, sale_date, amount) VALUES
(101, '2023-01-01', 100.00),
(102, '2023-01-01', 150.00),
(101, '2023-01-02', 120.00),
(103, '2023-01-02', 200.00),
(101, '2023-01-03', 110.00),
(102, '2023-01-03', 160.00),
(101, '2023-01-04', 130.00);

Here’s how you’d get that running total using a window function:

SELECT
    product_id,
    sale_date,
    amount,
    SUM(amount) OVER (PARTITION BY product_id ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM sales
ORDER BY product_id, sale_date;

This query produces:

product_id sale_date amount running_total
101 2023-01-01 100.00 100.00
101 2023-01-02 120.00 220.00
101 2023-01-03 110.00 330.00
101 2023-01-04 130.00 460.00
102 2023-01-01 150.00 150.00
102 2023-01-03 160.00 310.00
103 2023-01-02 200.00 200.00

The core problem window functions solve is performing calculations across a set of table rows that are related to the current row, without collapsing those rows into a single output row like a standard GROUP BY aggregation. Think of it as a "group" that doesn’t discard the individual row details.

The OVER clause is the magic ingredient. It defines the "window" or set of rows the function operates on.

  • PARTITION BY product_id: This divides the rows into partitions based on product_id. The SUM() function will restart for each unique product_id.
  • ORDER BY sale_date: Within each partition, rows are ordered by sale_date. This is crucial for ordered operations like running totals.
  • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: This frame clause specifies which rows within the ordered partition are included in the calculation for the current row. UNBOUNDED PRECEDING means start from the very first row in the partition, and CURRENT ROW means include the current row itself.

This allows for powerful analyses like calculating moving averages, ranking rows, finding the difference from a previous row, and much more, all within a single SQL query against your existing SQLite database.

Many developers overlook the flexibility of the frame clause in OVER. While ORDER BY is often sufficient for simple running totals, explicitly defining the frame with ROWS BETWEEN ... AND ... or RANGE BETWEEN ... AND ... provides granular control. For instance, you can define a frame that only looks at the previous 5 rows (ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING), or a frame that includes all rows with the same value as the current row in the ORDER BY column (RANGE BETWEEN CURRENT ROW AND CURRENT ROW). This precise control is what unlocks many advanced analytical patterns.

Understanding the nuances of PARTITION BY and ORDER BY within the OVER clause is key to unlocking complex analytical patterns in SQLite. The next step is often exploring aggregate window functions and their analytic counterparts.

Want structured learning?

Take the full Sqlite course →