SELECT
Every builder starts as a SELECT — QueryBuilder::<Dialect>::table("users") with no other method calls compiles to SELECT * FROM "users". This page covers everything that shapes the SELECT list: plain columns, aliases, aggregates, raw expressions, subquery columns, and DISTINCT / DISTINCT ON. Filtering belongs to WHERE, combining tables to JOIN.
select — plain columns
select takes any iterable of column names and replaces the column list. Every name is escaped per dialect; dotted identifiers are escaped per segment; a literal "*" passes through unquoted; an empty list selects *:
let (sql, binds) = QueryBuilder::<Postgres>::table("users")
.select(["id", "name"])
.where_eq("status", "active")
.to_sql();
// SELECT "id", "name" FROM "users" WHERE "status" = $1let (sql, _) = QueryBuilder::<Postgres>::table("users")
.select(["users.id"])
.to_sql();
// SELECT "users"."id" FROM "users"Escaping is not optional decoration — a hostile column name is neutralized by quote-doubling rather than spliced in:
let (sql, _) = QueryBuilder::<Postgres>::table("users")
.select([r#"id" ; DROP TABLE users; --"#])
.to_sql();
// SELECT "id"" ; DROP TABLE users; --" FROM "users"select_as — column aliases
select_as(col, alias) emits col AS alias, with both identifiers escaped:
let (sql, _) = QueryBuilder::<Postgres>::table("users")
.select_as("created_at", "joined")
.to_sql();
// SELECT "created_at" AS "joined" FROM "users"Aggregates
Five aggregate helpers — select_count, select_sum, select_avg, select_min, select_max — each with an _as twin that adds an escaped alias. The column is escaped; for select_count (and select_count_as) the special column "*" passes through as COUNT(*):
let (sql, _) = QueryBuilder::<Postgres>::table("users")
.select_count("*")
.to_sql();
// SELECT COUNT(*) FROM "users"let (sql, _) = QueryBuilder::<Postgres>::table("orders")
.select_sum("amount")
.select_avg("amount")
.select_min("amount")
.select_max("amount")
.to_sql();
// SELECT SUM("amount"), AVG("amount"), MIN("amount"), MAX("amount") FROM "orders"Aggregates mix freely with plain columns and GROUP BY (see GROUP BY · HAVING · ORDER · LIMIT):
let (sql, _) = QueryBuilder::<Postgres>::table("orders")
.select(["status"])
.select_count_as("*", "cnt")
.select_sum_as("amount", "total")
.group_by(["status"])
.to_sql();
// SELECT "status", COUNT(*) AS "cnt", SUM("amount") AS "total" FROM "orders" GROUP BY "status"Qualified columns work inside aggregates too: select_sum("orders.amount") → SUM("orders"."amount").
select_raw — verbatim expressions
select_raw(sql, binds) is the escape hatch for anything the structured API does not cover (functions, casts, window expressions). The fragment is appended to the column list after any select columns and aggregate/alias expressions; multiple calls accumulate; binds is an Option<Vec<Value>> (None for no binds).
⚠️ Positional placeholder contract
The
sqlfragment is emitted verbatim — it is NOT escaped and NOT renumbered. Its binds are appended to the running bind list in order. On Postgres you must hand-write$Nnumbers that match the actual bind position (number of binds already accumulated + 1, + 2, …); on MySQL/SQLite use?. A wrong$Nproduces a malformed query. The same contract applies to every*_rawmethod — see the Security Model for the full escape-hatch inventory.
let (sql, _) = QueryBuilder::<Postgres>::table("orders")
.select_raw("COUNT(*)", None)
.to_sql();
// SELECT COUNT(*) FROM "orders"select_subquery — scalar subquery columns
select_subquery(alias, sub) takes another builder by value and emits (<sub>) AS "alias" after the regular columns and any select_raw expressions. The subquery is compiled with placeholder continuity: since the SELECT list renders before WHERE, the subquery's binds take the earlier $N numbers — automatically, no manual numbering involved:
let (sql, binds) = QueryBuilder::<Postgres>::table("users")
.select(["id"])
.select_subquery(
"cnt",
QueryBuilder::<Postgres>::table("orders")
.select_raw("COUNT(*)", None)
.where_eq("status", 1i64),
)
.where_eq("active", true)
.to_sql();
// SELECT "id", (SELECT COUNT(*) FROM "orders" WHERE "status" = $1) AS "cnt" FROM "users" WHERE "active" = $2
// binds == [Value::I64(1), Value::Bool(true)]Correlated subqueries use where_column to reference the outer table — see WHERE.
distinct
distinct() emits SELECT DISTINCT … and works on all dialects:
let (sql, _) = QueryBuilder::<Postgres>::table("t")
.distinct()
.select(["a"])
.to_sql();
// SELECT DISTINCT "a" FROM "t"distinct_on — Postgres only
distinct_on(cols) emits Postgres' SELECT DISTINCT ON (cols) …; the columns are escaped like any identifier:
let (sql, _) = QueryBuilder::<Postgres>::table("t")
.distinct_on(["a", "b"])
.select(["a"])
.to_sql();
// SELECT DISTINCT ON ("a", "b") "a" FROM "t"Dialect note — MySQL and SQLite have no
DISTINCT ON. Compiling adistinct_onbuilder against either dialect fails:try_to_sql()returnsBuildError::DistinctOnRequiresPostgres, andto_sql()panics withDISTINCT ON requires PostgreSQL. If the dialect is decided at runtime, use thetry_form.
Related pages
- WHERE — filtering the rows you select
- JOIN — combining tables
- GROUP BY · HAVING · ORDER · LIMIT — shaping the result set
- Error Handling —
to_sql()vstry_to_sql()andBuildError - Security Model — what
select_rawdoes and does not protect