Track a table and you get a query language over it — filtering, sorting, pagination, aggregation and nesting — without writing a resolver. This lesson is that language, with real responses from a running engine.
Selecting
query {
properties(limit: 2) {
title
city
pricePerNight
}
}{"data":{"properties":[
{"title":"Modern Condo, Downtown Skyline","city":"Seattle","pricePerNight":198.00},
{"title":"Historic Adobe near the Plaza","city":"Santa Fe","pricePerNight":225.00}]}}Filtering with where
Every column gets a set of comparison operators.
query {
properties(where: {city: {_eq: "Seattle"}}) { title pricePerNight }
}The common ones:
{city: {_eq: "Seattle"}}
{pricePerNight: {_lte: 250}}
{city: {_in: ["Seattle", "Maui"]}}
{title: {_ilike: "%villa%"}} # case-insensitive
{deletedAt: {_isNull: true}}
{status: {_neq: "DRAFT"}}Combine them with _and, _or and _not:
query {
properties(
where: {
_and: [
{city: {_eq: "Seattle"}},
{pricePerNight: {_lte: 250}},
{_not: {roomType: {_eq: "SHARED"}}}
]
}
) { title pricePerNight roomType }
}Several conditions in one object are already _and-ed, so
{city: {_eq: "Seattle"}, bedrooms: {_gte: 2}} means both. You need explicit
_and only when you want two conditions on the same field.
Sorting and pagination
query {
properties(orderBy: {ratingAverage: DESC}, limit: 2) {
title
city
ratingAverage
}
}{"data":{"properties":[
{"title":"Oceanfront Villa, Private Steps to Sand","city":"Maui","ratingAverage":4.98},
{"title":"Lakefront A-Frame","city":"Lake Tahoe","ratingAverage":4.96}]}}The argument name depends on a setting
That is orderBy with an uppercase DESC, and most examples you will find
online say order_by: {rating_average: desc}. Both are correct — for different
engines.
With naming_convention set to graphql-default, Hasura camelCases field
names and arguments and uppercases enum values. Without it you get the database’s
snake_case throughout. Copying a snippet from documentation written under the other convention
produces 'properties' has no argument named 'order_by', which reads like a version
problem and is not one.
Sort by several columns in order:
properties(orderBy: [{city: ASC}, {pricePerNight: DESC}])Paginate with limit and offset. Offset pagination gets slow and can skip
rows when data changes underneath it, so for long lists prefer keyset pagination — sort by
something unique and ask for rows after the last one you saw:
properties(where: {createdAt: {_lt: $lastSeen}}, orderBy: {createdAt: DESC}, limit: 20)Aggregates
Every tracked table gets an Aggregate field alongside it.
query {
propertiesAggregate {
aggregate {
count
avg { pricePerNight }
}
}
}{"data":{"propertiesAggregate":{"aggregate":{
"count":12,"avg":{"pricePerNight":261.6666666666667}}}}}min, max, sum and stddev work the same way, and
aggregates accept the same where as the plain field. You can also return
nodes alongside aggregate to get the count and the page in one request
— which is exactly what a paginated list UI needs.
Variables
Never interpolate values into a query string. Use variables — they are typed, cacheable, and they keep the query text stable, which matters later for allow-lists.
query Search($city: String!, $max: numeric!) {
properties(where: {_and: [{city: {_eq: $city}}, {pricePerNight: {_lte: $max}}]}) {
title
pricePerNight
}
}Note numeric. Hasura’s scalar names come from Postgres types, so a
numeric column is not Float and a timestamptz is
timestamptz. Guessing Float gets a type error.
The thing that is not in the schema
Run this unauthenticated and it fails:
{"errors":[{"message":"field 'bookings' not found in type: 'query_root'",
"extensions":{"path":"$.selectionSet.bookings","code":"validation-failed"}}]}Not “permission denied”, and not an empty list. Permissions shape the schema per role,
so a role with no permission on bookings has no such field. Keep it in mind while
querying: what you can see depends entirely on who the engine thinks you are.
Queries in v3 (DDN)
On the v3 sections. Everything marked v3 (DDN) is taken from the official Hasura DDN documentation as read on 2026-08-21 and was not run locally — it shows configuration, never claimed output. The v2 material was executed against a running engine.
The query language is deliberately close — Hasura describes the generated v3 schema as compatible with v2 schemas — but where the capability comes from is different. In v2 every column is filterable and sortable because the engine generated it. In v3 you declare it.
kind: BooleanExpressionType
version: v1
definition:
name: PropertiesBoolExp
operand:
object:
type: Properties
comparableFields:
- fieldName: city
booleanExpressionType: StringBoolExp
- fieldName: pricePerNight
booleanExpressionType: NumericBoolExpSorting has its own object, OrderByExpression, and aggregation has
AggregateExpression. Aggregates in v3 are marked connector-dependent: what you get
depends on what the connector implements, rather than being guaranteed by the engine.
That is more verbose, and the trade is deliberate — you say which fields are filterable instead of exposing every column to every predicate by default.
Next
Lesson 5 covers the part that makes this worth using: relationships.