Applies to VAST Cluster 5.5.
Functions › Aggregate and analytic functions › Sums and averages. Adds up the non-NULL values of a number column or expression. On a BOOLEAN, it counts the true values.
Syntax
sum(x)
| Argument | Type | Description |
|---|---|---|
x |
a number (any integer type, FLOAT, DOUBLE, DECIMAL) or a BOOLEAN |
The values to add up. NULLs are skipped. |
Returns: depends on the type of x:
x |
Result | In Python (ADBC) |
|---|---|---|
TINYINT to BIGINT, UTINYINT to UBIGINT, HUGEINT |
HUGEINT, a 128-bit integer |
Decimal |
BOOLEAN |
HUGEINT: the number of true values |
Decimal |
FLOAT, DOUBLE |
DOUBLE |
float |
DECIMAL(p, s) |
DECIMAL(38, s) |
Decimal |
Set up the examples
The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_aggregate_sum. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.
This block connects, defines the run() and plan() helpers, and creates a table of four order lines: a quantity, a unit price, a weight in kilograms, whether the line is paid, and the IDs of the items shipped. The last row has no values:
import os
import re
import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb
BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_aggregate_sum"'
conn = dbapi.connect(
driver=adbc_driver_vastdb.get_driver_path(),
db_kwargs={
"vast.db.endpoint": os.environ["QE_ENDPOINT"],
"vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
"vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
},
autocommit=True,
)
def server_message(e):
"""The server's message in a driver error, without the request ID or the echoed statement."""
text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
found = (re.search(r'message: "(.*?)", request_id', text, re.S)
or re.search(r"description: (.*)\)", text, re.S))
message = found.group(1) if found else text.strip().splitlines()[-1]
message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
return message.replace(BUCKET, "<bucket>")
def run(sql):
"""Run one statement. Print the rows it returns, or the error."""
try:
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall() if cur.description else []
except (adbc_driver_manager.Error, OSError) as e:
print(f"{type(e).__name__}: {server_message(e)}")
else:
print(rows)
def plan(sql):
"""Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
with conn.cursor() as cur:
cur.execute("EXPLAIN " + sql)
steps = cur.fetchall()[0][1]
print("\n".join(line for line in steps.splitlines() if "Rows" not in line))
TABLE = f"{SCHEMA}.order_lines"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} "
"(id BIGINT, qty INTEGER, price DECIMAL(10,2), weight DOUBLE, paid BOOLEAN, items BIGINT[])")
run(f"INSERT INTO {TABLE} VALUES "
"(1, 2, 9.99, 1.5, true, [101, 102]), "
"(2, 1, 24.50, 0.25, false, [103]), "
"(3, 5, 3.10, 2.0, true, []), "
"(4, NULL, NULL, NULL, NULL, NULL)")
[]
[]
[(4,)]
What it does
sum(x) is an aggregate: it adds up the values of x over the rows of the query, skips the NULLs, and returns one value. x can be a column or an expression, such as qty * price or CASE WHEN ... THEN 1 ELSE 0 END.
The result type is wide enough that integer sums don't overflow in practice: a sum of BIGINT values is a 128-bit HUGEINT, so two rows at the largest BIGINT, 9223372036854775807, add up to 18446744073709551614 without an error. A DECIMAL(p, s) sum keeps the scale s and widens to 38 digits, and is exact: ten rows of 0.10 add up to 1.00. A DOUBLE sum is a floating-point sum: it can round, and it doesn't fail on overflow (see NULLs and edge cases).
On a BOOLEAN, true counts as 1 and false as 0, so sum(paid) is the number of paid lines.
NULLs and edge cases
| Input | Result |
|---|---|
| NULL values | Skipped |
Only NULLs, or sum(NULL) |
NULL |
No rows (an empty table, or a WHERE that matches nothing) |
NULL; use coalesce(sum(x), 0) for 0 |
sum(42) with no FROM |
42 |
A constant over a table: sum(9223372036854775807) |
The constant times the number of rows, as a HUGEINT |
A DOUBLE sum too large |
inf, not an error |
NaN among the values, or inf and -inf |
nan |
A DECIMAL(38, s) sum too large |
Error: Overflow in HUGEINT addition: ... |
An integer expression that overflows before the sum: sum(id * 9223372036854775807) |
Error: Overflow in multiplication of INT64 (...)! |
| A string, date, time, timestamp, interval or list | Error: No function matches the given name and argument types, for example 'sum(VARCHAR)' |
sum(DISTINCT x) |
Error: The operator 'UNGROUPED_AGGREGATE' type=8 is currently not supported for this usage. |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run(f"SELECT sum(qty), sum(price), sum(weight), sum(paid) FROM {TABLE}")
run(f"SELECT sum(qty) FROM {TABLE} WHERE id = 4")
run(f"SELECT sum(qty), coalesce(sum(qty), 0) FROM {TABLE} WHERE id > 100")
run("SELECT sum(NULL), sum(42)")
run(f"SELECT sum(9223372036854775807) FROM {TABLE}")
run("SELECT sum(x) FROM (SELECT 0.1::DOUBLE AS x FROM range(10))")
run("SELECT sum(x) FROM (SELECT 0.1::DECIMAL(10,2) AS x FROM range(10))")
run("SELECT sum(x) FROM (VALUES (1.0e308::DOUBLE), (1.0e308::DOUBLE)) v(x)")
run("SELECT sum(x) FROM (VALUES ('inf'::DOUBLE), ('-inf'::DOUBLE)) v(x)")
run("SELECT sum(x) FROM (VALUES (99999999999999999999999999999999999999::DECIMAL(38,0)), "
"(99999999999999999999999999999999999999::DECIMAL(38,0))) v(x)")
run(f"SELECT sum(id * 9223372036854775807) FROM {TABLE}")
run(f"SELECT sum('10') FROM {TABLE}")
run(f"SELECT sum(DISTINCT qty) FROM {TABLE}")
[(Decimal('8'), Decimal('37.59'), 3.75, Decimal('2'))]
[(None,)]
[(None, Decimal('0'))]
[(None, Decimal('42'))]
[(Decimal('36893488147419103228'),)]
[(1.0,)]
[(Decimal('1.00'),)]
[(inf,)]
[(nan,)]
OSError: Overflow in HUGEINT addition: 99999999999999999999999999999999999999 + 99999999999999999999999999999999999999
OSError: Overflow in multiplication of INT64 (2 * 9223372036854775807)!
InternalError: Binder Error: No function matches the given name and argument types 'sum(VARCHAR)'. You might need to add explicit type casts.
InternalError: The operator 'UNGROUPED_AGGREGATE' type=8 is currently not supported for this usage.
Examples
The total quantity, the total of the unit prices, the total weight and the number of paid lines:
run(f"SELECT sum(qty), sum(price), sum(weight), sum(paid) FROM {TABLE}")
[(Decimal('8'), Decimal('37.59'), 3.75, Decimal('2'))]
The value of all the lines, and of the paid ones only, from an expression. qty * price is an INTEGER times a DECIMAL(10,2), so the sum is a DECIMAL and exact:
run(f"SELECT sum(qty * price), sum(qty * price) FILTER (WHERE paid) FROM {TABLE}")
[(Decimal('59.98'), Decimal('35.48'))]
The number of items shipped over all the lines, and the total of their IDs. sum() doesn't take a list: unnest the lists into rows first, or use list_sum to add up the elements of each list. list_sum of an empty list is NULL:
run(f"SELECT count(*), sum(x) FROM (SELECT unnest(items) AS x FROM {TABLE})")
run(f"SELECT id, list_sum(items) FROM {TABLE} ORDER BY id LIMIT 10")
[(3, Decimal('306'))]
[(1, Decimal('203')), (2, Decimal('103')), (3, None), (4, None)]
How it runs in VAST
The Query Engine runs on the cluster's CNodes. A sum query has two steps, which EXPLAIN shows:
- A scan in VAST DataBase (
VAST_SCAN). It reads only the columns the sum uses, and evaluates theWHEREconditions it can push down, so rows that don't match never leave storage. - One aggregate step (
UNGROUPED_AGGREGATE) in the Query Engine. It computes the expression insidesum()for each row the scan sends, adds it up, and returns one row.
The plan of a sum with a pushed-down WHERE condition. The condition doesn't appear in the plan, because the scan evaluates it:
plan(f"SELECT sum(weight) FROM {TABLE} WHERE id > 1")
┌───────────────────────────┐
│ UNGROUPED_AGGREGATE │
│ ──────────────────── │
│ Aggregates: │
│ sum(weight) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: weight │
│ │
└───────────────────────────┘
Make it faster
- Filter in
WHERE, not inFILTER (WHERE ...).sum(x) FILTER (WHERE id > 1)andsum(x) ... WHERE id > 1give the same result, but theFILTERcondition is evaluated in the aggregate step, after the scan has sent every row, and theWHEREcondition in VAST DataBase. UseFILTERwhen you need different conditions for different sums in one query:
plan(f"SELECT sum(weight) FILTER (WHERE id > 1) FROM {TABLE}")
┌───────────────────────────┐
│ UNGROUPED_AGGREGATE │
│ ──────────────────── │
│ Aggregates: │
│ sum(weight) FILTER (WHERE │
│ (id > 1)) Filter: (id > 1)│
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ id │
│ weight │
│ │
└───────────────────────────┘
- Keep functions out of the
WHEREcondition.WHERE abs(weight) > 1isn't pushed down: the plan shows aFILTERstep in the Query Engine above the scan. Compare the raw column with a constant when you can. - Compute several sums in one query. They share one scan and one aggregate step.
- Sort the table on the column you filter by. Sorting lets the scan skip whole blocks of rows; see indexing and data skipping.
Limitations and errors
- Numbers and
BOOLEANonly. A string, a date, a time, a timestamp, an interval or a list fails withNo function matches the given name and argument types; the error lists the typessumtakes. A number stored as text needs a cast:sum(CAST(s AS BIGINT)). - Overflow inside the expression.
sum()itself doesn't overflow on integers, but the expression inside it is computed in the column's type first:id * 9223372036854775807overflowsBIGINTbefore the sum sees it. Cast toDOUBLEfirst if an approximate result is enough:sum(id::DOUBLE * 9223372036854775807). DECIMAL(38, s)sums can overflow. A sum beyond 38 digits fails withOverflow in HUGEINT addition.DOUBLEsums round. Use aDECIMALcolumn for money and other values that must add up exactly.fsum,kahan_sumandsumkahanaren't available (not available).- No
GROUP BYorDISTINCT. A sum per group fails withThe operator 'HASH_GROUP_BY' type=9 is currently not supported.;sum(DISTINCT x)fails withThe operator 'UNGROUPED_AGGREGATE' type=8 is currently not supported for this usage. - Window forms.
sum(x) OVER (ROWS UNBOUNDED PRECEDING)runs, as a running total in the order the scan returns the rows.OVER (),OVER (ORDER BY ...)andOVER (PARTITION BY ...)fail withThe operator 'WINDOW' type=6 is currently not supported.See window functions.
run(f"SELECT sum(id::DOUBLE * 9223372036854775807) FROM {TABLE}")
run(f"SELECT sum(items) FROM {TABLE}")
run(f"SELECT paid, sum(qty) FROM {TABLE} GROUP BY paid")
run(f"SELECT id, sum(qty) OVER (ORDER BY id) FROM {TABLE}")
[(9.223372036854776e+19,)]
InternalError: Binder Error: No function matches the given name and argument types 'sum(BIGINT[])'. You might need to add explicit type casts.
InternalError: The operator 'HASH_GROUP_BY' type=9 is currently not supported.
InternalError: The operator 'WINDOW' type=6 is currently not supported.
Similar functions
| Function | Returns |
|---|---|
sum(x) |
the total of the non-NULL values |
avg (x) |
the total divided by the number of non-NULL values, as a DOUBLE |
product (x) |
the values multiplied together, as a DOUBLE |
count_if (condition) |
the number of rows where the condition is true; the same as sum(CASE WHEN condition THEN 1 ELSE 0 END) |
list_sum (list) |
the total of the elements of one list, per row |
run(f"SELECT sum(qty), avg(qty), product(qty), sum(CASE WHEN paid THEN 1 ELSE 0 END), count_if(paid) FROM {TABLE}")
run("SELECT list_sum([1, 2, 3])")
[(Decimal('8'), 2.6666666666666665, 10.0, Decimal('2'), Decimal('2'))]
[(Decimal('6'),)]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]