Hasura – Queries

July 3, 20263 min readUpdated 8/21/2026

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: NumericBoolExp

Sorting 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.