Applies to VAST Cluster 5.5.
Functions › Aggregate and analytic functions › Sums and averages. Returns a weighted mean: each value counts in proportion to its weight. weighted_avg is the same function.
Syntax
wavg(value, weight)
weighted_avg(value, weight)
| Argument | Type | Description |
|---|---|---|
value |
a number | The values to average. |
weight |
a number | The weight of each value. Rows where value or weight is NULL are skipped. |
Returns: DOUBLE.
Aliases: weighted_avg.
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_wavg. 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 five course grades: a grade point score and the course's credits. Course 4 has no score yet, and course 5 has no credits:
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_wavg"'
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}.grades"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, course VARCHAR, score DOUBLE, credits DOUBLE)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, 'Algebra', 3.5, 4.0), "
"(2, 'Drawing', 2.0, 2.0), "
"(3, 'Physics', 4.0, 1.0), "
"(4, 'History', NULL, 3.0), "
"(5, 'Seminar', 3.0, NULL)")
[]
[]
[(5,)]
What it does
wavg(value, weight) is an aggregate: it multiplies each value by its weight, adds up the products, and divides by the total of the weights. The plan shows exactly what it computes:
sum(value * weight) / sum(CASE WHEN value IS NOT NULL THEN weight ELSE 0 END)
So a row counts only when both its value and its weight are known. A row with a NULL value is left out of both sums, and so is a row with a NULL weight. This is why sum(value * weight) / sum(weight) isn't the same thing: it divides by the weights of the rows with no value too.
With every weight 1, wavg(value, 1) is the plain mean, avg. Negative weights are accepted: wavg(value, -weight) gives the same result as wavg(value, weight), because both sums change sign.
wavg is a macro, a shorthand the Query Engine expands into those two sums, not an aggregate of its own. That's why it can't take FILTER, DISTINCT or ORDER BY (see Limitations and errors).
NULLs and edge cases
| Input | Result |
|---|---|
| A row with a NULL value or a NULL weight | Skipped |
| Every value or every weight NULL | NULL |
No rows (an empty table, or a WHERE that matches nothing) |
NULL |
Weights that add up to 0, as in wavg(x, 0) |
nan |
wavg(x, 1) |
The same as avg(x) |
wavg(x, -w) |
The same as wavg(x, w) |
NaN among the values |
nan |
A string, date, BOOLEAN or list |
Error: No function matches the given name and argument types, naming the multiplication, for example '*(VARCHAR, DOUBLE)' |
| One argument, or three | Error: Macro function wavg(value, weight) requires 2 positional arguments, ... |
wavg(x, w) FILTER (WHERE ...) |
Error: Function "wavg" is a Macro Function. "DISTINCT", "FILTER", and "ORDER BY" are only applicable to aggregate functions. |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run(f"SELECT wavg(score, credits), weighted_avg(score, credits) FROM {TABLE}")
run(f"SELECT wavg(score, credits) FROM {TABLE} WHERE id IN (4, 5)")
run(f"SELECT wavg(score, credits) FROM {TABLE} WHERE id > 100")
run(f"SELECT wavg(score, 0), wavg(score, 1), avg(score), wavg(score, -credits) FROM {TABLE}")
run(f"SELECT wavg(course, credits) FROM {TABLE}")
run(f"SELECT wavg(score) FROM {TABLE}")
run(f"SELECT wavg(score, credits) FILTER (WHERE id < 3) FROM {TABLE}")
[(3.142857142857143, 3.142857142857143)]
[(None,)]
[(None,)]
[(nan, 3.125, 3.125, 3.142857142857143)]
InternalError: Binder Error: No function matches the given name and argument types '*(VARCHAR, DOUBLE)'. You might need to add explicit type casts.
InternalError: Binder Error: Macro function wavg(value, weight) requires 2 positional arguments, but a single positional argument was provided.
InternalError: Invalid Input Error: Function "wavg" is a Macro Function. "DISTINCT", "FILTER", and "ORDER BY" are only applicable to aggregate functions.
Examples
The credit-weighted grade point average, next to the plain mean of the scores and the formula written out:
run(f"SELECT wavg(score, credits), avg(score), "
f"sum(score * credits) / sum(credits) FILTER (WHERE score IS NOT NULL) FROM {TABLE}")
[(3.142857142857143, 3.125, 3.142857142857143)]
The weighted average of the first three courses only. wavg can't take FILTER, so restrict the rows in WHERE:
run(f"SELECT wavg(score, credits) FROM {TABLE} WHERE id <= 3")
[(3.142857142857143,)]
How it runs in VAST
The Query Engine runs on the cluster's CNodes. A wavg query has three steps, which EXPLAIN shows:
- A scan in VAST DataBase (
VAST_SCAN), which reads the value and weight columns and evaluates theWHEREconditions it can push down. - One aggregate step (
UNGROUPED_AGGREGATE) in the Query Engine, which computes the two sums: ofvalue * weight, and of the weights of the rows that have a value. - A
PROJECTIONstep, which divides the first sum by the second.
plan(f"SELECT wavg(score, credits) FROM {TABLE} WHERE id <= 3")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ (sum((score * credits)) / │
│ sum(CASE WHEN ((score IS│
│ NOT NULL)) THEN (credits)│
│ ELSE CAST(0 AS DOUBLE) │
│ END)) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ UNGROUPED_AGGREGATE │
│ ──────────────────── │
│ Aggregates: │
│ sum((score * credits)) │
│ sum(CASE WHEN ((score IS │
│ NOT NULL)) THEN (credits)│
│ ELSE 0.0 END) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ score │
│ credits │
│ │
└───────────────────────────┘
Make it faster
- Restrict the rows in
WHERE. A comparison of a column with a constant is evaluated in VAST DataBase, so the rows that don't match never leave storage. It's also the only way to restrictwavg, since it can't takeFILTER. - Compute it with other aggregates in one query.
wavg,avg,sumand the others in oneSELECTlist share one scan. - Write the formula out to restrict different averages differently.
sum(score * credits) FILTER (WHERE c) / sum(credits) FILTER (WHERE c AND score IS NOT NULL)giveswavgfor the rows wherecholds, and lets one query compute several of them.
Limitations and errors
- No
FILTER,DISTINCTorORDER BY. They fail withFunction "wavg" is a Macro Function. "DISTINCT", "FILTER", and "ORDER BY" are only applicable to aggregate functions.(weighted_avgin the message when you use that name). UseWHERE, or write the formula out withFILTERon each sum. - No window form.
wavg(x, w) OVER (...)fails withCatalog Error: / is not an aggregate function. - Numbers only. A string, a date, a
BOOLEANor a list fails withNo function matches the given name and argument types, and the message names the multiplication inside the macro, such as'*(VARCHAR, DOUBLE)'. - Integer overflow. The product
value * weightis computed in the columns' type: two largeBIGINTcolumns can fail withOverflow in multiplication of INT64 (...)!. Cast one of them toDOUBLEfirst. - No
GROUP BY. A weighted average per group fails withThe operator 'HASH_GROUP_BY' type=9 is currently not supported.
run(f"SELECT id, wavg(score, credits) OVER (ROWS UNBOUNDED PRECEDING) FROM {TABLE}")
run(f"SELECT course, wavg(score, credits) FROM {TABLE} GROUP BY course")
InternalError: Catalog Error: / is not an aggregate function
InternalError: The operator 'HASH_GROUP_BY' type=9 is currently not supported.
Similar functions
| Function | Returns |
|---|---|
wavg(x, w), weighted_avg(x, w) |
the mean of x weighted by w, over the rows where both are known |
avg (x) |
the plain mean: every value has weight 1 |
sum(x * w) / sum(w) (sum) |
divides by the weights of the rows with no value too; not a weighted mean when x has NULLs |
geomean (x) |
the geometric mean of positive values |
run(f"SELECT wavg(score, credits), avg(score), wavg(score, 1), sum(score * credits) / sum(credits), "
f"geomean(score) FROM {TABLE}")
[(3.142857142857143, 3.125, 3.125, 2.2, 3.0274001040350913)]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]