zoobzio December 12, 2025 Edit this page

Aggregates

ASTQL supports SQL aggregate functions, GROUP BY, HAVING, and window functions.

Aggregate Functions

Basic Aggregates

FunctionSQLDescription
Sum(field)SUM("field")Sum of values
Avg(field)AVG("field")Average of values
Min(field)MIN("field")Minimum value
Max(field)MAX("field")Maximum value
CountField(field)COUNT("field")Count of non-null values
CountDistinct(field)COUNT(DISTINCT "field")Count of unique values

Using Aggregates

result, _ := astql.Select(instance.T("orders")).
    Fields(instance.F("user_id")).
    SelectExpr(astql.As(astql.Sum(instance.F("total")), "total_spent")).
    SelectExpr(astql.As(astql.CountField(instance.F("id")), "order_count")).
    GroupBy(instance.F("user_id")).
    Render()

// SELECT "user_id", SUM("total") AS "total_spent", COUNT("id") AS "order_count"
// FROM "orders"
// GROUP BY "user_id"

Aliases

Use As() to add an alias:

astql.As(astql.Sum(instance.F("amount")), "total_amount")
// SUM("amount") AS "total_amount"

GROUP BY

Group results by one or more fields:

result, _ := astql.Select(instance.T("orders")).
    Fields(instance.F("user_id"), instance.F("status")).
    SelectExpr(astql.As(astql.CountField(instance.F("id")), "count")).
    GroupBy(instance.F("user_id"), instance.F("status")).
    Render()

// SELECT "user_id", "status", COUNT("id") AS "count"
// FROM "orders"
// GROUP BY "user_id", "status"

HAVING

Filter grouped results:

Simple HAVING

result, _ := astql.Select(instance.T("orders")).
    Fields(instance.F("user_id")).
    SelectExpr(astql.As(astql.Sum(instance.F("total")), "sum")).
    GroupBy(instance.F("user_id")).
    Having(instance.C(instance.F("total"), astql.GT, instance.P("min_total"))).
    Render()

// SELECT "user_id", SUM("total") AS "sum"
// FROM "orders"
// GROUP BY "user_id"
// HAVING "total" > :min_total

Aggregate HAVING

For conditions on aggregate functions, use HavingAgg:

result, _ := astql.Select(instance.T("orders")).
    Fields(instance.F("user_id")).
    SelectExpr(astql.As(astql.CountField(instance.F("id")), "order_count")).
    GroupBy(instance.F("user_id")).
    HavingAgg(astql.HavingCount(astql.GT, instance.P("min_orders"))).
    Render()

// SELECT "user_id", COUNT("id") AS "order_count"
// FROM "orders"
// GROUP BY "user_id"
// HAVING COUNT(*) > :min_orders

HAVING Helpers

FunctionSQL
HavingCount(op, param)COUNT(*) op :param
HavingCountField(field, op, param)COUNT("field") op :param
HavingCountDistinct(field, op, param)COUNT(DISTINCT "field") op :param
HavingSum(field, op, param)SUM("field") op :param
HavingAvg(field, op, param)AVG("field") op :param
HavingMin(field, op, param)MIN("field") op :param
HavingMax(field, op, param)MAX("field") op :param

FILTER Clause

PostgreSQL's FILTER clause for conditional aggregation:

result, _ := astql.Select(instance.T("orders")).
    Fields(instance.F("user_id")).
    SelectExpr(astql.As(
        astql.SumFilter(
            instance.F("total"),
            instance.C(instance.F("status"), astql.EQ, instance.P("completed")),
        ),
        "completed_total",
    )).
    GroupBy(instance.F("user_id")).
    Render()

// SELECT "user_id",
//        SUM("total") FILTER (WHERE "status" = :completed) AS "completed_total"
// FROM "orders"
// GROUP BY "user_id"

Available filter variants:

  • SumFilter(field, condition)
  • AvgFilter(field, condition)
  • MinFilter(field, condition)
  • MaxFilter(field, condition)
  • CountFieldFilter(field, condition)
  • CountDistinctFilter(field, condition)

Window Functions

Window functions compute values across related rows.

Basic Window Functions

result, _ := astql.Select(instance.T("employees")).
    Fields(instance.F("name"), instance.F("department"), instance.F("salary")).
    SelectExpr(
        astql.RowNumber().
            OverBuilder(astql.Window().
                PartitionBy(instance.F("department")).
                OrderBy(instance.F("salary"), astql.DESC)).
            As("rank"),
    ).
    Render()

// SELECT "name", "department", "salary",
//        ROW_NUMBER() OVER (PARTITION BY "department" ORDER BY "salary" DESC) AS "rank"
// FROM "employees"

Window Function Types

FunctionSQL
RowNumber()ROW_NUMBER()
Rank()RANK()
DenseRank()DENSE_RANK()
Ntile(param)NTILE(:param)
Lag(field, offset) or Lag(field, offset, default)LAG(field, :offset) or LAG(field, :offset, :default)
Lead(field, offset) or Lead(field, offset, default)LEAD(field, :offset) or LEAD(field, :offset, :default)
FirstValue(field)FIRST_VALUE(field)
LastValue(field)LAST_VALUE(field)

Aggregate Window Functions

// Running total
astql.SumOver(instance.F("amount")).
    OverBuilder(astql.Window().
        PartitionBy(instance.F("account_id")).
        OrderBy(instance.F("date"), astql.ASC)).
    As("running_total")

// SUM("amount") OVER (PARTITION BY "account_id" ORDER BY "date" ASC) AS "running_total"

Available: SumOver, AvgOver, CountOver, MinOver, MaxOver

Window Specification

Build window specs with Window():

spec := astql.Window().
    PartitionBy(instance.F("department")).
    OrderBy(instance.F("hire_date"), astql.ASC).
    OrderByNulls(instance.F("salary"), astql.DESC, astql.NullsLast).
    Rows(astql.FrameUnboundedPreceding, astql.FrameCurrentRow).
    Build()

Frame Bounds

ConstantSQL
FrameUnboundedPrecedingUNBOUNDED PRECEDING
FrameCurrentRowCURRENT ROW
FrameUnboundedFollowingUNBOUNDED FOLLOWING

Math Functions

Available Functions

FunctionSQL
Round(field) or Round(field, precision)ROUND("field") or ROUND("field", :precision)
Floor(field)FLOOR("field")
Ceil(field)CEIL("field")
Abs(field)ABS("field")
Power(field, exponent)POWER("field", :exponent)
Sqrt(field)SQRT("field")

Example

result, _ := astql.Select(instance.T("products")).
    SelectExpr(astql.As(astql.Round(instance.F("price"), instance.P("decimals")), "rounded")).
    SelectExpr(astql.As(astql.Floor(instance.F("rating")), "floor_rating")).
    Render()

// SELECT ROUND("price", :decimals) AS "rounded",
//        FLOOR("rating") AS "floor_rating"
// FROM "products"

CASE Expressions

Conditional logic in SELECT:

caseExpr := astql.Case().
    When(
        instance.C(instance.F("score"), astql.GE, instance.P("high")),
        instance.P("grade_a"),
    ).
    When(
        instance.C(instance.F("score"), astql.GE, instance.P("mid")),
        instance.P("grade_b"),
    ).
    Else(instance.P("grade_c")).
    As("grade").
    Build()

result, _ := astql.Select(instance.T("students")).
    Fields(instance.F("name")).
    SelectExpr(caseExpr).
    Render()

// SELECT "name",
//        CASE WHEN "score" >= :high THEN :grade_a
//             WHEN "score" >= :mid THEN :grade_b
//             ELSE :grade_c END AS "grade"
// FROM "students"

Type Casting

Cast fields to different types:

astql.Cast(instance.F("created_at"), astql.CastDate)
// CAST("created_at" AS DATE)

Available cast types: CastText, CastInteger, CastBigint, CastNumeric, CastBoolean, CastDate, CastTimestamp, CastTimestampTZ, CastUUID, CastJSON, CastJSONB, and more.

COALESCE and NULLIF

// COALESCE - first non-null value
astql.Coalesce(instance.P("preferred"), instance.P("default"))
// COALESCE(:preferred, :default)

// NULLIF - returns NULL if values are equal
astql.NullIf(instance.P("value"), instance.P("sentinel"))
// NULLIF(:value, :sentinel)