Skip to main content

Sorting & Pagination

Hugr provides comprehensive sorting and pagination capabilities to control the order and size of query results. You can sort by multiple fields, including nested fields from relations, and paginate results using limit and offset.

Basic Sorting​

Sort by Single Field​

Sort results using the order_by argument:

query {
customers(order_by: [{ field: "name", direction: ASC }]) {
id
name
email
}
}

Direction options:

  • ASC - Ascending order (A-Z, 0-9, oldest to newest)
  • DESC - Descending order (Z-A, 9-0, newest to oldest)

Sort by Multiple Fields​

Specify multiple sorting criteria (applied in order):

query {
customers(order_by: [
{ field: "country", direction: ASC }
{ field: "city", direction: ASC }
{ field: "name", direction: ASC }
]) {
id
name
city
country
}
}

Equivalent to SQL:

ORDER BY country ASC, city ASC, name ASC

Sorting by Field Types​

Numeric Fields​

query {
products(order_by: [{ field: "price", direction: DESC }]) {
id
name
price
}
}

String Fields​

query {
customers(order_by: [{ field: "name", direction: ASC }]) {
id
name
}
}

String sorting is:

  • Case-sensitive by default
  • Follows database collation rules
  • Alphabetical order (A-Z or Z-A)

Date and Timestamp Fields​

query {
orders(order_by: [{ field: "created_at", direction: DESC }]) {
id
created_at
status
}
}

Boolean Fields​

query {
products(order_by: [
{ field: "in_stock", direction: DESC } # true before false
{ field: "name", direction: ASC }
]) {
id
name
in_stock
}
}

One-to-One Relations​

Sort by fields in related objects:

query {
orders(order_by: [
{ field: "customer.country", direction: ASC }
{ field: "customer.name", direction: ASC }
]) {
id
customer {
country: country
name
}
total
}
}

Important: The related field must be selected in the query:

# Valid - customer.name is selected
query {
orders(order_by: [{ field: "customer.name", direction: ASC }]) {
id
customer {
name # ✓ Selected
}
}
}

# Invalid - customer.name not selected
query {
orders(order_by: [{ field: "customer.name", direction: ASC }]) {
id
customer {
id # ✗ name not selected
}
}
}

Multiple Relation Levels​

query {
order_details(order_by: [
{ field: "product.category.name", direction: ASC }
{ field: "product.name", direction: ASC }
]) {
id
product {
name
category {
name
}
}
}
}

Sorting by Aggregated Relations​

Sort by aggregation results:

query {
customers(order_by: [
{ field: "orders_aggregation.total.sum", direction: DESC }
]) {
id
name
orders_aggregation {
total {
sum # Must be selected
}
}
}
}

Field Aliases in ORDER BY​

When using field aliases, reference the alias in order_by:

query {
products(order_by: [
{ field: "category_name", direction: ASC }
]) {
id
name
category {
category_name: name # Alias
}
}
}

Nested Sorting​

For subqueries, use order_by or nested_order_by:

order_by in Subqueries​

Applies before the join:

query {
customers {
id
name
orders(
order_by: [{ field: "created_at", direction: DESC }]
limit: 5
) {
id
created_at
}
}
}

nested_order_by​

Applies after the join:

query {
customers {
id
name
orders(
nested_order_by: [{ field: "created_at", direction: DESC }]
nested_limit: 5
) {
id
created_at
}
}
}

Basic Pagination​

Using Limit​

Limit the number of results:

query {
customers(limit: 10) {
id
name
}
}

Default limits:

  • Maximum: 2000 records per query
  • No default limit (returns all records up to maximum)

Using Offset​

Skip a number of records:

query {
customers(
limit: 10
offset: 20
) {
id
name
}
}

Returns records 21-30.

Offset-Based Pagination​

Page-by-Page Navigation​

query GetPage($limit: Int!, $offset: Int!) {
customers(
order_by: [{ field: "id", direction: ASC }]
limit: $limit
offset: $offset
) {
id
name
email
}
}

Variables for page 3 with 20 items per page (offset calculated client-side):

{
"limit": 20,
"offset": 40
}

Calculate offset in your application code:

const page = 2; // Zero-indexed: page 0, 1, 2, ...
const pageSize = 20;
const offset = page * pageSize; // 2 * 20 = 40

Calculating Total Pages​

Use aggregation to get total count:

query GetPageWithTotal($limit: Int!, $offset: Int!) {
customers(
order_by: [{ field: "id", direction: ASC }]
limit: $limit
offset: $offset
) {
id
name
}

customers_aggregation {
_rows_count
}
}

Calculate pages and offset in your application:

const page = 2;
const pageSize = 20;
const offset = page * pageSize;

// Execute query with calculated offset
const result = await query({
limit: pageSize,
offset: offset
});

// Calculate total pages
const totalPages = Math.ceil(
result.customers_aggregation._rows_count / pageSize
);

Cursor-Based Pagination​

For better performance with large datasets, use cursor-based pagination:

query GetCustomersAfter($cursor: Int!, $limit: Int!) {
customers(
filter: { id: { gt: $cursor } }
order_by: [{ field: "id", direction: ASC }]
limit: $limit
) {
id
name
email
}
}

Get next page using last ID as cursor:

{
"cursor": 100,
"limit": 20
}

Bi-Directional Cursor Pagination​

# Forward pagination
query GetNext($cursor: Int!, $limit: Int!) {
customers(
filter: { id: { gt: $cursor } }
order_by: [{ field: "id", direction: ASC }]
limit: $limit
) {
id
name
}
}

# Backward pagination
query GetPrevious($cursor: Int!, $limit: Int!) {
customers(
filter: { id: { lt: $cursor } }
order_by: [{ field: "id", direction: DESC }]
limit: $limit
) {
id
name
}
}

Distinct Results​

DISTINCT ON Specific Fields​

Get unique values for specified fields:

query {
customers(distinct_on: ["country"]) {
country
}
}

Returns one customer per unique country.

Multiple DISTINCT Fields​

query {
customers(distinct_on: ["country", "city"]) {
country
city
}
}

Returns one customer per unique country-city combination.

DISTINCT with ORDER BY​

Combine DISTINCT with sorting:

query {
orders(
distinct_on: ["customer_id"]
order_by: [
{ field: "customer_id", direction: ASC }
{ field: "created_at", direction: DESC }
]
) {
customer_id
created_at
total
}
}

Returns the most recent order for each customer.

Important: When using distinct_on, the first order_by field must be one of the distinct_on fields.

Nested Pagination​

Limit Subquery Results​

query {
customers {
id
name
# Get only 5 most recent orders
recent_orders: orders(
order_by: [{ field: "created_at", direction: DESC }]
limit: 5
) {
id
created_at
}
}
}

nested_limit and nested_offset​

Control pagination after joins:

query {
customers(limit: 10) {
id
name
orders(
nested_order_by: [{ field: "created_at", direction: DESC }]
nested_limit: 5
nested_offset: 0
) {
id
created_at
}
}
}

Difference between limit and nested_limit:

  • limit - Applied before join (limits orders before joining to customers)
  • nested_limit - Applied after join (limits orders per customer)

Sorting Aggregations​

Sort Bucket Aggregations​

query {
orders_bucket_aggregation(
order_by: [
{ field: "aggregations.total.sum", direction: DESC }
]
limit: 10
) {
key {
status
}
aggregations {
_rows_count
total {
sum # Must be selected
}
}
}
}

Sort by Multiple Aggregations​

query {
products_bucket_aggregation(
order_by: [
{ field: "aggregations._rows_count", direction: DESC }
{ field: "aggregations.price.avg", direction: ASC }
]
) {
key {
category {
name
}
}
aggregations {
_rows_count
price {
avg
}
}
}
}

Sort by Key Fields​

query {
orders_bucket_aggregation(
order_by: [
{ field: "key.customer.country", direction: ASC }
{ field: "aggregations.total.sum", direction: DESC }
]
) {
key {
customer {
country
}
}
aggregations {
total {
sum
}
}
}
}

Performance Considerations​

1. Use Indexes for Sorting​

Ensure indexes exist on sorted fields:

-- PostgreSQL example
CREATE INDEX idx_customers_name ON customers(name);
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);

2. Limit Results​

Always use limit to prevent large result sets:

# Good
query {
customers(limit: 100) {
id
name
}
}

# Avoid - May return millions of rows
query {
customers {
id
name
}
}

3. Use Cursor Pagination for Large Datasets​

Offset pagination becomes slow with high offsets:

# Slow for large offsets
query {
customers(
order_by: [{ field: "id", direction: ASC }]
offset: 1000000 # Slow!
limit: 20
) {
id
}
}

# Faster with cursor
query {
customers(
filter: { id: { gt: 1000000 } }
order_by: [{ field: "id", direction: ASC }]
limit: 20
) {
id
}
}

4. Limit Nested Queries​

Always limit subquery results:

query {
customers(limit: 10) {
id
orders(limit: 10) { # ✓ Limit subquery
id
}
}
}

5. Sort by Indexed Fields​

Prefer sorting by indexed fields:

# Good - Primary key is indexed
query {
customers(order_by: [{ field: "id", direction: ASC }]) {
id
}
}

# May be slow - Computed fields aren't indexed
query {
customers(order_by: [{ field: "full_name", direction: ASC }]) {
id
}
}

Common Patterns​

Most Recent Records​

query {
orders(
order_by: [{ field: "created_at", direction: DESC }]
limit: 20
) {
id
created_at
}
}

Top N by Value​

query {
products(
order_by: [{ field: "sales_count", direction: DESC }]
limit: 10
) {
id
name
sales_count
}
}

Alphabetical List​

query {
customers(
order_by: [{ field: "name", direction: ASC }]
) {
id
name
}
}

Random Sample​

While Hugr doesn't have built-in random ordering, you can:

  1. Get total count
  2. Generate random offsets
  3. Fetch individual records
query {
# Get total
customers_aggregation {
_rows_count
}
}

# Then fetch random offset
query GetRandom($randomOffset: Int!) {
customers(
offset: $randomOffset
limit: 1
) {
id
name
}
}

Next Steps​

  • Learn about Relations for working with related data
  • See Aggregations for sorting aggregated results
  • Check Filtering to combine with sorting and pagination