Aggregations & GROUP BY¶
ASQL provides a clean syntax for grouping and aggregating data, with natural language options for common patterns.
Basic GROUP BY¶
Use group by with parentheses to define aggregations:
from orders
group by customer_id (
sum(amount) as total_spent,
# as order_count,
avg(amount) as avg_order
)
The syntax is: group by <columns> ( <aggregations> ).
Count Shorthand (#, ##, uniq)¶
The # symbol provides flexible counting syntax. Use ## (double hash) or uniq() for distinct counts.
Basic Row Count¶
from users
group by country (
# as user_count -- COUNT(*)
)
Count with Column¶
Use #col or # col to count non-null values in a column:
#email -- COUNT(email)
# email -- COUNT(email) (with space)
#(email) -- COUNT(email) (with parens)
Distinct Count (## or uniq)¶
Use double hash ## or the uniq() function for distinct counts:
##user_id -- COUNT(DISTINCT user_id)
## user_id -- COUNT(DISTINCT user_id) (with space)
#(distinct email) -- COUNT(DISTINCT email) (explicit)
uniq(user_id) -- COUNT(DISTINCT user_id)
uniq user_id -- COUNT(DISTINCT user_id) (space notation)
Count Shorthand Reference¶
| Syntax | SQL Output | Use Case |
|---|---|---|
# |
COUNT(*) |
Count all rows |
#* or # * |
COUNT(*) |
Explicit row count |
#col or # col |
COUNT(col) |
Count non-null values |
#(col) |
COUNT(col) |
Count with parens |
##col or ## col |
COUNT(DISTINCT col) |
Distinct count |
#(distinct col) |
COUNT(DISTINCT col) |
Explicit distinct |
uniq(col) |
COUNT(DISTINCT col) |
Function form |
uniq col |
COUNT(DISTINCT col) |
Space notation |
Multiple Grouping Columns¶
Group by multiple columns separated by commas:
from sales
group by region, year(date) (
sum(amount) as revenue,
# as transactions
)
Aggregate Functions¶
| Function | Description | Example |
|---|---|---|
# or count(*) |
Count rows | # as total (preferred) |
count(col) |
Count non-null values | count(email) |
count(distinct col) |
Count distinct values | count(distinct customer_id) |
sum(col) |
Sum values | sum(amount) |
avg(col) |
Average | avg(price) |
min(col) |
Minimum | min(date) |
max(col) |
Maximum | max(amount) |
Natural Language Aggregates¶
ASQL supports natural language syntax for aggregations:
-- All of these are equivalent:
sum(amount)
sum amount
sum_amount
sum of amount
Using shorthand:
from sales
group by region (
sum_amount,
avg_price,
# as total
)
Auto-Aliasing¶
When you use functions without explicit AS aliases, ASQL automatically generates meaningful column names:
from orders
group by region (
sum(amount), -- → column: sum_amount
avg(price), -- → column: avg_price
count(*) -- → column: num
)
order by -sum_amount -- Reference the auto-generated alias!
This solves SQL's problem of unusable default names like count, f0_, or SUM(amount). Auto-aliases follow the {func}_{col} pattern and can be referenced in ORDER BY, WHERE, and subsequent queries.
See the Auto-Aliasing Reference for the complete mapping table.
Function Aliases¶
Natural language aliases map to SQL functions:
| Alias | SQL Function |
|---|---|
total |
SUM |
average |
AVG |
maximum |
MAX |
minimum |
MIN |
from sales
group by product (
total amount as revenue,
average price as avg_price
)
Grouping by Expressions¶
Group by computed values like date truncations:
from orders
group by month(created_at) (
sum(amount) as revenue
)
Group by multiple expressions:
from orders
group by year(created_at), month(created_at) (
sum(amount) as revenue,
# as order_count
)
Aliasing Group Columns¶
Use as to alias group columns:
from orders
group by month(created_at) (
sum(amount) as revenue
)
This is especially useful for date truncations where you want a clean column name.
HAVING (Filtering After Grouping)¶
Filter grouped results using another where clause after grouping:
from orders
group by customer_id (
sum(amount) as total_spent
)
where total_spent > 1000
This compiles to a HAVING clause or a wrapping CTE with WHERE.
Aggregations Without GROUP BY¶
You can use aggregates without grouping to get totals:
from orders
select
sum(amount) as total_revenue,
# as total_rows, -- COUNT(*) - total number of rows
avg(amount) as avg_order_value
Important: # by itself is always COUNT(*) (row count). For distinct counts, use ## or uniq():
- # → COUNT(*) (row count)
- ##col or uniq(col) → COUNT(DISTINCT col) (distinct count)
Conditional Aggregates¶
Use when inside aggregates for conditional counting/summing:
from orders
group by customer_id (
# as total_orders,
sum(status = "completed" ? 1 : 0) as completed_orders,
sum(status = "returned" ? amount : 0) as returned_amount
)
Guaranteed Groups¶
By default, ASQL ensures all expected dimension values appear in grouped results—even if they have no data. See Guaranteed Groups for details.
-- All months from Jan-Jun will appear, even with zero revenue
from orders
where order_date >= @2024-01-01 and order_date < @2024-07-01
group by month(order_date) (
sum(amount) ?? 0 as revenue
)
Default Column Selection¶
If you don't specify a select after grouping, ASQL returns all grouping columns followed by all aggregations:
from orders
group by region (
sum(amount) as revenue,
# as orders
)
-- Returns: region, revenue, orders
Real-World Examples¶
Top Customers by Revenue¶
from orders
where status = "completed"
group by customer_id (
sum(amount) as total_spent,
# as order_count
)
order by -total_spent
limit 10
Monthly Revenue Trend¶
from orders
where year(created_at) = 2024
group by month(created_at) (
sum(amount) as revenue,
# as orders,
avg(amount) as avg_order
)
order by month
Category Performance¶
from products
& orders on products.id = orders.product_id
group by products.category (
sum(orders.amount) as revenue,
count(distinct orders.customer_id) as customers,
# as transactions
)
order by -revenue
Next Steps¶
- Window Functions — Running totals, ranking, and more
- Dates & Time — Date truncation and grouping
- Guaranteed Groups — Complete dimension coverage