Running totals, also known as cumulative sums, are essential for financial reporting, inventory tracking, and data analysis tasks where you need to see accumulated values over time.
This article explores various methods to calculate running totals in Oracle SQL, from traditional approaches to modern window functions, complete with practical examples and performance considerations.
What Is a Running Total and Why Is It Important?
A running total represents the cumulative sum of values up to and including the current row in a dataset.
Unlike simple aggregations that provide a single total, running totals show how values accumulate progressively, making them invaluable for trend analysis, balance calculations, and progressive reporting.

Financial analysts use running totals to track account balances, inventory managers monitor stock levels, and sales teams analyze cumulative revenue throughout periods.
How Does the SUM() Window Function Calculate Running Totals?
Oracle's window functions provide the most elegant solution for calculating running totals using the SUM() function with an OVER clause.
Data Preparation
Let's create a sales table to demonstrate running total calculations:
-- Create the sales table
CREATE TABLE exrt_sales (
sale_id NUMBER PRIMARY KEY,
sale_date DATE,
product_name VARCHAR2(50),
amount NUMBER(10,2)
);
-- Insert sample data
INSERT INTO exrt_sales VALUES (1, DATE '2025-01-05', 'Laptop', 1200.00);
INSERT INTO exrt_sales VALUES (2, DATE '2025-01-08', 'Mouse', 45.00);
INSERT INTO exrt_sales VALUES (3, DATE '2025-01-10', 'Keyboard', 85.00);
INSERT INTO exrt_sales VALUES (4, DATE '2025-01-15', 'Monitor', 350.00);
INSERT INTO exrt_sales VALUES (5, DATE '2025-01-18', 'Laptop', 1150.00);
INSERT INTO exrt_sales VALUES (6, DATE '2025-01-20', 'Mouse', 40.00);
INSERT INTO exrt_sales VALUES (7, DATE '2025-01-22', 'Headphones', 120.00);
INSERT INTO exrt_sales VALUES (8, DATE '2025-01-25', 'Monitor', 380.00);
COMMIT;
-- Display the data SELECT * FROM exrt_sales ORDER BY sale_date;
| SALE_ID | SALE_DATE | PRODUCT_NAME | AMOUNT |
|---|---|---|---|
| 1 | 05-JAN-25 | Laptop | 1200.00 |
| 2 | 08-JAN-25 | Mouse | 45.00 |
| 3 | 10-JAN-25 | Keyboard | 85.00 |
| 4 | 15-JAN-25 | Monitor | 350.00 |
| 5 | 18-JAN-25 | Laptop | 1150.00 |
| 6 | 20-JAN-25 | Mouse | 40.00 |
| 7 | 22-JAN-25 | Headphones | 120.00 |
| 8 | 25-JAN-25 | Monitor | 380.00 |
Basic Running Total Calculation
This query calculates a running total of sales amounts ordered by date:
SELECT
sale_id,
sale_date,
product_name,
amount,
SUM(amount) OVER (ORDER BY sale_date) AS running_total
FROM exrt_sales
ORDER BY sale_date;
| SALE_ID | SALE_DATE | PRODUCT_NAME | AMOUNT | RUNNING_TOTAL |
|---|---|---|---|---|
| 1 | 05-JAN-25 | Laptop | 1200.00 | 1200.00 |
| 2 | 08-JAN-25 | Mouse | 45.00 | 1245.00 |
| 3 | 10-JAN-25 | Keyboard | 85.00 | 1330.00 |
| 4 | 15-JAN-25 | Monitor | 350.00 | 1680.00 |
| 5 | 18-JAN-25 | Laptop | 1150.00 | 2830.00 |
| 6 | 20-JAN-25 | Mouse | 40.00 | 2870.00 |
| 7 | 22-JAN-25 | Headphones | 120.00 | 2990.00 |
| 8 | 25-JAN-25 | Monitor | 380.00 | 3370.00 |
The OVER clause with ORDER BY creates a window frame that includes all rows from the beginning up to the current row.
What Are the Different Window Frame Options for Running Totals?
Window frames define which rows to include in the calculation, offering flexibility in how running totals are computed.
Understanding ROWS and RANGE Clauses
The default behavior uses RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, but you can explicitly specify the frame:
SELECT
sale_id,
sale_date,
amount,
SUM(amount) OVER (
ORDER BY sale_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total_rows,
SUM(amount) OVER (
ORDER BY sale_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total_range
FROM exrt_sales
ORDER BY sale_date;
| SALE_ID | SALE_DATE | AMOUNT | RUNNING_TOTAL_ROWS | RUNNING_TOTAL_RANGE |
|---|---|---|---|---|
| 1 | 05-JAN-25 | 1200.00 | 1200.00 | 1200.00 |
| 2 | 08-JAN-25 | 45.00 | 1245.00 | 1245.00 |
| 3 | 10-JAN-25 | 85.00 | 1330.00 | 1330.00 |
| 4 | 15-JAN-25 | 350.00 | 1680.00 | 1680.00 |
| 5 | 18-JAN-25 | 1150.00 | 2830.00 | 2830.00 |
| 6 | 20-JAN-25 | 40.00 | 2870.00 | 2870.00 |
| 7 | 22-JAN-25 | 120.00 | 120.00 | 2990.00 |
| 8 | 25-JAN-25 | 380.00 | 3370.00 | 3370.00 |
ROWS considers physical row positions, while RANGE considers logical values, which matters when dealing with duplicate order values.
How Can You Calculate Running Totals with Partitions?
Partitioned running totals reset the calculation for each group, useful for category-wise or period-wise analysis.
Running Totals by Product Category
This query calculates separate running totals for each product:
SELECT
sale_id,
sale_date,
product_name,
amount,
SUM(amount) OVER (
PARTITION BY product_name
ORDER BY sale_date
) AS product_running_total
FROM exrt_sales
ORDER BY product_name, sale_date;
| SALE_ID | SALE_DATE | PRODUCT_NAME | AMOUNT | PRODUCT_RUNNING_TOTAL |
|---|---|---|---|---|
| 7 | 22-JAN-25 | Headphones | 120.00 | 120.00 |
| 3 | 10-JAN-25 | Keyboard | 85.00 | 85.00 |
| 1 | 05-JAN-25 | Laptop | 1200.00 | 1200.00 |
| 5 | 18-JAN-25 | Laptop | 1150.00 | 2350.00 |
| 4 | 15-JAN-25 | Monitor | 350.00 | 350.00 |
| 8 | 25-JAN-25 | Monitor | 380.00 | 730.00 |
| 2 | 08-JAN-25 | Mouse | 45.00 | 45.00 |
| 6 | 20-JAN-25 | Mouse | 40.00 | 85.00 |
Each product maintains its own running total, resetting when a new product category begins.
What Are Alternative Methods to Calculate Running Totals?
While window functions are preferred, understanding alternative approaches helps with legacy systems or specific requirements.
Using Self-Joins
Before window functions, self-joins were the primary method for calculating running totals:
SELECT
s1.sale_id,
s1.sale_date,
s1.product_name,
s1.amount,
SUM(s2.amount) AS running_total
FROM exrt_sales s1
JOIN exrt_sales s2 ON s2.sale_date <= s1.sale_date
GROUP BY s1.sale_id, s1.sale_date, s1.product_name, s1.amount
ORDER BY s1.sale_date;
| SALE_ID | SALE_DATE | PRODUCT_NAME | AMOUNT | RUNNING_TOTAL |
|---|---|---|---|---|
| 1 | 05-JAN-25 | Laptop | 1200.00 | 1200.00 |
| 2 | 08-JAN-25 | Mouse | 45.00 | 1245.00 |
| 3 | 10-JAN-25 | Keyboard | 85.00 | 1330.00 |
| 4 | 15-JAN-25 | Monitor | 350.00 | 1680.00 |
| 5 | 18-JAN-25 | Laptop | 1150.00 | 2830.00 |
| 6 | 20-JAN-25 | Mouse | 40.00 | 2870.00 |
| 7 | 22-JAN-25 | Headphones | 120.00 | 2990.00 |
| 8 | 25-JAN-25 | Monitor | 380.00 | 3370.00 |
This method works but performs poorly on large datasets due to the exponential growth of join operations.
Using Correlated Subqueries
Another traditional approach uses correlated subqueries:
SELECT
sale_id,
sale_date,
product_name,
amount,
(SELECT SUM(amount)
FROM exrt_sales s2
WHERE s2.sale_date <= s1.sale_date) AS running_total
FROM exrt_sales s1
ORDER BY sale_date;
| SALE_ID | SALE_DATE | PRODUCT_NAME | AMOUNT | RUNNING_TOTAL |
|---|---|---|---|---|
| 1 | 05-JAN-25 | Laptop | 1200.00 | 1200.00 |
| 2 | 08-JAN-25 | Mouse | 45.00 | 1245.00 |
| 3 | 10-JAN-25 | Keyboard | 85.00 | 1330.00 |
| 4 | 15-JAN-25 | Monitor | 350.00 | 1680.00 |
| 5 | 18-JAN-25 | Laptop | 1150.00 | 2830.00 |
| 6 | 20-JAN-25 | Mouse | 40.00 | 2870.00 |
| 7 | 22-JAN-25 | Headphones | 120.00 | 2990.00 |
| 8 | 25-JAN-25 | Monitor | 380.00 | 3370.00 |
Like self-joins, this method suffers from performance issues as the subquery executes for each row.
How Do You Handle Complex Running Total Scenarios?
Real-world applications often require more sophisticated running total calculations beyond simple cumulative sums.
Running Totals with Conditional Logic
Let's create an inventory table to demonstrate running totals with conditions:
-- Create inventory movements table
CREATE TABLE exrt_inventory (
movement_id NUMBER PRIMARY KEY,
movement_date DATE,
product_name VARCHAR2(50),
movement_type VARCHAR2(10),
quantity NUMBER
);
-- Insert sample data
INSERT INTO exrt_inventory VALUES (1, DATE '2025-01-01', 'Widget', 'IN', 100);
INSERT INTO exrt_inventory VALUES (2, DATE '2025-01-05', 'Widget', 'OUT', 20);
INSERT INTO exrt_inventory VALUES (3, DATE '2025-01-08', 'Widget', 'IN', 50);
INSERT INTO exrt_inventory VALUES (4, DATE '2025-01-10', 'Widget', 'OUT', 30);
INSERT INTO exrt_inventory VALUES (5, DATE '2025-01-12', 'Widget', 'OUT', 25);
INSERT INTO exrt_inventory VALUES (6, DATE '2025-01-15', 'Widget', 'IN', 40);
COMMIT;
-- Display the data SELECT * FROM exrt_inventory ORDER BY movement_date;
| MOVEMENT_ID | MOVEMENT_DATE | PRODUCT_NAME | MOVEMENT_TYPE | QUANTITY |
|---|---|---|---|---|
| 1 | 01-JAN-25 | Widget | IN | 100 |
| 2 | 05-JAN-25 | Widget | OUT | 20 |
| 3 | 08-JAN-25 | Widget | IN | 50 |
| 4 | 10-JAN-25 | Widget | OUT | 30 |
| 5 | 12-JAN-25 | Widget | OUT | 25 |
| 6 | 15-JAN-25 | Widget | IN | 40 |
This query calculates running inventory balance considering inbound and outbound movements:
SELECT
movement_id,
movement_date,
movement_type,
quantity,
SUM(CASE
WHEN movement_type = 'IN' THEN quantity
WHEN movement_type = 'OUT' THEN -quantity
END) OVER (ORDER BY movement_date) AS running_balance
FROM exrt_inventory
ORDER BY movement_date;
| MOVEMENT_ID | MOVEMENT_DATE | MOVEMENT_TYPE | QUANTITY | RUNNING_BALANCE |
|---|---|---|---|---|
| 1 | 01-JAN-25 | IN | 100 | 100 |
| 2 | 05-JAN-25 | OUT | 20 | 80 |
| 3 | 08-JAN-25 | IN | 50 | 130 |
| 4 | 10-JAN-25 | OUT | 30 | 100 |
| 5 | 12-JAN-25 | OUT | 25 | 75 |
| 6 | 15-JAN-25 | IN | 40 | 115 |
Running Percentage Calculations
Running totals often combine with percentage calculations for deeper insights:
SELECT
sale_id,
sale_date,
product_name,
amount,
SUM(amount) OVER (ORDER BY sale_date) AS running_total,
ROUND(
100 * SUM(amount) OVER (ORDER BY sale_date) /
SUM(amount) OVER (), 2
) AS running_percentage
FROM exrt_sales
ORDER BY sale_date;
| SALE_ID | SALE_DATE | PRODUCT_NAME | AMOUNT | RUNNING_TOTAL | RUNNING_PERCENTAGE |
|---|---|---|---|---|---|
| 1 | 05-JAN-25 | Laptop | 1200.00 | 1200.00 | 35.61 |
| 2 | 08-JAN-25 | Mouse | 45.00 | 1245.00 | 36.94 |
| 3 | 10-JAN-25 | Keyboard | 85.00 | 1330.00 | 39.47 |
| 4 | 15-JAN-25 | Monitor | 350.00 | 1680.00 | 49.85 |
| 5 | 18-JAN-25 | Laptop | 1150.00 | 2830.00 | 83.98 |
| 6 | 20-JAN-25 | Mouse | 40.00 | 2870.00 | 85.16 |
| 7 | 22-JAN-25 | Headphones | 120.00 | 2990.00 | 88.72 |
| 8 | 25-JAN-25 | Monitor | 380.00 | 3370.00 | 100.00 |
This calculation shows what percentage of total sales has been achieved at each point in time.
What Performance Considerations Apply to Running Total Queries?
Performance optimization becomes critical when dealing with large datasets or complex running total calculations.
Indexing Strategies
Creating appropriate indexes significantly improves running total query performance:
-- Create index on date column for time-based running totals CREATE INDEX idx_exrt_sales_date ON exrt_sales(sale_date); -- Create composite index for partitioned running totals CREATE INDEX idx_exrt_sales_product_date ON exrt_sales(product_name, sale_date);
Indexes on ORDER BY and PARTITION BY columns enable Oracle to efficiently sort and group data for window function calculations.
Materialized Views for Frequent Calculations
For frequently accessed running totals, materialized views provide pre-calculated results:
CREATE MATERIALIZED VIEW exrt_sales_running_totals
BUILD IMMEDIATE
REFRESH COMPLETE ON DEMAND
AS
SELECT
sale_id,
sale_date,
product_name,
amount,
SUM(amount) OVER (ORDER BY sale_date) AS running_total,
SUM(amount) OVER (PARTITION BY product_name ORDER BY sale_date) AS product_running_total
FROM exrt_sales;
This approach trades storage space for query performance, ideal for reporting scenarios where real-time updates aren't critical.
How Can You Apply Running Totals in Real-World Scenarios?
Understanding practical applications helps implement running totals effectively in business contexts.
Financial Balance Tracking
Let's create a bank account transactions table:
-- Create bank transactions table
CREATE TABLE exrt_bank_transactions (
transaction_id NUMBER PRIMARY KEY,
transaction_date DATE,
description VARCHAR2(100),
debit NUMBER(10,2),
credit NUMBER(10,2)
);
-- Insert sample data
INSERT INTO exrt_bank_transactions VALUES (1, DATE '2025-01-01', 'Opening Balance', 0, 5000);
INSERT INTO exrt_bank_transactions VALUES (2, DATE '2025-01-05', 'Salary Deposit', 0, 3500);
INSERT INTO exrt_bank_transactions VALUES (3, DATE '2025-01-08', 'Rent Payment', 1200, 0);
INSERT INTO exrt_bank_transactions VALUES (4, DATE '2025-01-10', 'Grocery Shopping', 250, 0);
INSERT INTO exrt_bank_transactions VALUES (5, DATE '2025-01-15', 'Freelance Income', 0, 800);
INSERT INTO exrt_bank_transactions VALUES (6, DATE '2025-01-18', 'Utilities', 150, 0);
INSERT INTO exrt_bank_transactions VALUES (7, DATE '2025-01-20', 'Online Purchase', 85, 0);
INSERT INTO exrt_bank_transactions VALUES (8, DATE '2025-01-25', 'Interest Credit', 0, 25);
COMMIT;
-- Display the data SELECT * FROM exrt_bank_transactions ORDER BY transaction_date;
| TRANSACTION_ID | TRANSACTION_DATE | DESCRIPTION | DEBIT | CREDIT |
|---|---|---|---|---|
| 1 | 01-JAN-25 | Opening Balance | 0 | 5000 |
| 2 | 05-JAN-25 | Salary Deposit | 0 | 3500 |
| 3 | 08-JAN-25 | Rent Payment | 1200 | 0 |
| 4 | 10-JAN-25 | Grocery Shopping | 250 | 0 |
| 5 | 15-JAN-25 | Freelance Income | 0 | 800 |
| 6 | 18-JAN-25 | Utilities | 150 | 0 |
| 7 | 20-JAN-25 | Online Purchase | 85 | 0 |
| 8 | 25-JAN-25 | Interest Credit | 0 | 25 |
This query calculates the running balance for the bank account:
SELECT
transaction_id,
transaction_date,
description,
debit,
credit,
credit - debit AS net_amount,
SUM(credit - debit) OVER (ORDER BY transaction_date, transaction_id) AS balance
FROM exrt_bank_transactions
ORDER BY transaction_date, transaction_id;
| TRANSACTION_ID | TRANSACTION_DATE | DESCRIPTION | DEBIT | CREDIT | NET_AMOUNT | BALANCE |
|---|---|---|---|---|---|---|
| 1 | 01-JAN-25 | Opening Balance | 0 | 5000 | 5000 | 5000 |
| 2 | 05-JAN-25 | Salary Deposit | 0 | 3500 | 3500 | 8500 |
| 3 | 08-JAN-25 | Rent Payment | 1200 | 0 | -1200 | 7300 |
| 4 | 10-JAN-25 | Grocery Shopping | 250 | 0 | -250 | 7050 |
| 5 | 15-JAN-25 | Freelance Income | 0 | 800 | 800 | 7850 |
| 6 | 18-JAN-25 | Utilities | 150 | 0 | -150 | 7700 |
| 7 | 20-JAN-25 | Online Purchase | 85 | 0 | -85 | 7615 |
| 8 | 25-JAN-25 | Interest Credit | 0 | 25 | 25 | 7640 |
Sales Performance Analysis
Running totals help analyze sales performance against targets:
-- Create sales targets table
CREATE TABLE exrt_sales_targets (
month_year VARCHAR2(7),
target_amount NUMBER(10,2)
);
INSERT INTO exrt_sales_targets VALUES ('2025-01', 3000);
COMMIT;
-- Calculate running total with target comparison
WITH monthly_sales AS (
SELECT
TO_CHAR(sale_date, 'YYYY-MM') AS month_year,
sale_date,
amount,
SUM(amount) OVER (
PARTITION BY TO_CHAR(sale_date, 'YYYY-MM')
ORDER BY sale_date
) AS month_running_total
FROM exrt_sales
)
SELECT
ms.sale_date,
ms.amount,
ms.month_running_total,
st.target_amount,
ROUND(100 * ms.month_running_total / st.target_amount, 2) AS target_percentage
FROM monthly_sales ms
JOIN exrt_sales_targets st ON ms.month_year = st.month_year
ORDER BY ms.sale_date;
| SALE_DATE | AMOUNT | MONTH_RUNNING_TOTAL | TARGET_AMOUNT | TARGET_PERCENTAGE |
|---|---|---|---|---|
| 05-JAN-25 | 1200.00 | 1200.00 | 3000 | 40.00 |
| 08-JAN-25 | 45.00 | 1245.00 | 3000 | 41.50 |
| 10-JAN-25 | 85.00 | 1330.00 | 3000 | 44.33 |
| 15-JAN-25 | 350.00 | 1680.00 | 3000 | 56.00 |
| 18-JAN-25 | 1150.00 | 2830.00 | 3000 | 94.33 |
| 20-JAN-25 | 40.00 | 2870.00 | 3000 | 95.67 |
| 22-JAN-25 | 120.00 | 2990.00 | 3000 | 99.67 |
| 25-JAN-25 | 380.00 | 3370.00 | 3000 | 112.33 |
Conclusion
Running totals in Oracle SQL serve as fundamental tools for cumulative analysis across various business domains, from financial reporting to inventory management.
Window functions, particularly the SUM() OVER() construct, provide the most efficient and flexible approach for calculating running totals in modern Oracle databases.
While alternative methods like self-joins and correlated subqueries exist, they should generally be avoided due to performance limitations on larger datasets.
Understanding partitioning, window frames, and conditional logic enables developers to create sophisticated running total calculations tailored to specific business requirements.
Proper indexing, materialized views, and query optimization techniques ensure that running total calculations remain performant even as data volumes grow.
By mastering these techniques, database professionals can deliver powerful analytical capabilities that transform raw transactional data into meaningful cumulative insights for decision-making.



