> hypequery

Ordering

Sort results and paginate with orderBy, limit, and offset

Ordering

Control the order and quantity of results with sorting, limiting, and pagination tools.

Overview

Use ordering to:

  • Sort results by one or more columns
  • Limit the number of returned rows
  • Paginate through large datasets
  • Remove duplicates with distinct

Order By

Sort results by column values in ascending or descending order.

Single Column Sort

const users = await db.table('users')
  .select(['id', 'name', 'created_at'])
  .orderBy('created_at', 'DESC')
  .execute();

Ascending Order

const products = await db.table('products')
  .select(['id', 'name', 'price'])
  .orderBy('price', 'ASC')
  .execute();

Default Direction

If you don't specify a direction, orderBy defaults to ascending ('ASC').

Multiple Column Sort

Chain multiple orderBy() calls for secondary sorting:

const users = await db.table('users')
  .select(['id', 'name', 'country', 'created_at'])
  .orderBy('country', 'ASC')
  .orderBy('created_at', 'DESC')
  .execute();
// Sorts by country first, then by created_at within each country

Sort by Joined Columns

const results = await db.table('orders')
  .innerJoin('users', 'orders.user_id', 'users.id')
  .select([
    'orders.id',
    'users.name',
    'orders.total',
  ])
  .orderBy('users.name', 'ASC')
  .orderBy('orders.total', 'DESC')
  .execute();

Sort Direction Options

DirectionDescriptionExample
'ASC'Ascending (lowest to highest)orderBy('price', 'ASC')
'DESC'Descending (highest to lowest)orderBy('created_at', 'DESC')

Limit

Restrict the number of rows returned:

const latestUsers = await db.table('users')
  .select(['id', 'name', 'created_at'])
  .orderBy('created_at', 'DESC')
  .limit(10)
  .execute();

Performance

Always use limit() with orderBy() for predictable results. Without orderBy(), the rows returned may vary.

Limit with Aggregations

const topCategories = await db.table('orders')
  .innerJoin('products', 'orders.product_id', 'products.id')
  .select(['products.category'])
  .sum('orders.total', 'revenue')
  .groupBy(['products.category'])
  .orderBy('revenue', 'DESC')
  .limit(10)
  .execute();

Offset

Skip a specified number of rows before returning results:

const page2 = await db.table('users')
  .select(['id', 'name'])
  .orderBy('id', 'ASC')
  .limit(20)
  .offset(20)  // Skip first 20 rows
  .execute();

Offset Performance

Large offsets can be slow on big datasets. Consider keyset pagination for better performance.

Pagination

Combine limit() and offset() for pagination:

Basic Pagination

async function getUsers(page: number, pageSize: number) {
  const offset = (page - 1) * pageSize;

  return await db.table('users')
    .select(['id', 'name', 'email'])
    .orderBy('id', 'ASC')
    .limit(pageSize)
    .offset(offset)
    .execute();
}

// Page 1
const page1 = await getUsers(1, 20);

// Page 2
const page2 = await getUsers(2, 20);

Keyset Pagination (More Efficient)

Instead of offset, use the last seen value for better performance:

async function getUsersAfter(lastId: number, limit: number) {
  return await db.table('users')
    .select(['id', 'name', 'email'])
    .where('id', 'gt', lastId)
    .orderBy('id', 'ASC')
    .limit(limit)
    .execute();
}

// First page
const page1 = await getUsersAfter(0, 20);
const lastId = page1[page1.length - 1].id;

// Next page
const page2 = await getUsersAfter(lastId, 20);

Pagination Strategy

  • Small datasets: Use offset/limit (simple)
  • Large datasets: Use keyset pagination (faster)
  • Real-time: Use cursor-based pagination with time-based columns

Distinct

Remove duplicate rows from results:

const countries = await db.table('users')
  .select(['country'])
  .distinct()
  .orderBy('country', 'ASC')
  .execute();

Distinct with Multiple Columns

const uniqueCombinations = await db.table('orders')
  .select(['country', 'status'])
  .distinct()
  .execute();

Distinct with Aggregations

const stats = await db.table('events')
  .select(['user_id'])
  .distinctCount('event_type', 'unique_events')
  .groupBy(['user_id'])
  .orderBy('unique_events', 'DESC')
  .limit(10)
  .execute();

Common Patterns

Latest Records

const latestOrders = await db.table('orders')
  .select(['id', 'user_id', 'total', 'created_at'])
  .orderBy('created_at', 'DESC')
  .limit(50)
  .execute();

Top N by Value

const topSpenders = await db.table('orders')
  .select(['user_id'])
  .sum('total', 'lifetime_value')
  .groupBy(['user_id'])
  .orderBy('lifetime_value', 'DESC')
  .limit(100)
  .execute();

Most Recent Items Per User

const latestEvents = await db.table('events')
  .select(['user_id'])
  .max('created_at', 'last_event')
  .groupBy(['user_id'])
  .orderBy('last_event', 'DESC')
  .limit(50)
  .execute();

Random Sample

const sample = await db.table('users')
  .select(['id', 'name'])
  .orderBy('rand()', 'ASC')
  .limit(100)
  .execute();

Unique Values in Column

const countries = await db.table('users')
  .select(['country'])
  .where('country', 'isNotNull')
  .distinct()
  .orderBy('country', 'ASC')
  .execute();

Sort by Expression

const results = await db.table('products')
  .select(['id', 'name', 'price', 'discount'])
  .orderBy(selectExpr('price * (1 - discount)'), 'ASC')
  .execute();

Multiple Sorting Strategies

Primary and Secondary Sort

const results = await db.table('orders')
  .select(['id', 'status', 'total', 'created_at'])
  .orderBy('status', 'ASC')
  .orderBy('total', 'DESC')
  .orderBy('created_at', 'ASC')
  .execute();
// Sorts by status first, then by total within each status,
// then by created_at within same total

Conditional Sorting

const results = await db.table('products')
  .select(['id', 'name', 'price', 'stock'])
  .orderBy(selectExpr('if(stock > 0, 0, 1)'), 'ASC')  // In stock first
  .orderBy('price', 'ASC')
  .execute();

Type Safety

TypeScript ensures you sort by valid columns:

// ✅ Valid - column exists
await db.table('users')
  .orderBy('created_at', 'DESC')
  .execute();

// ❌ Error - column doesn't exist
await db.table('users')
  .orderBy('invalid_column', 'ASC')  // TypeScript error
  .execute();

// ✅ Valid - can sort by joined columns after joining
await db.table('orders')
  .innerJoin('users', 'orders.user_id', 'users.id')
  .orderBy('users.name', 'ASC')
  .execute();

// ❌ Error - can't sort by unjoined table
await db.table('orders')
  .orderBy('users.name', 'ASC')  // TypeScript error
  .execute();

Performance Considerations

Sort Performance

  • Sorting requires reading all matching rows
  • Use limit() to reduce work after sorting
  • Create indexes on frequently-sorted columns
  • Consider final() for ClickHouse-specific optimizations

Efficient Large Dataset Pagination

// ❌ Slow with large offsets
const page1000 = await db.table('events')
  .orderBy('created_at', 'DESC')
  .limit(20)
  .offset(20000)  // Skips 20,000 rows
  .execute();

// ✅ Faster - use keyset pagination
const lastTimestamp = '2024-01-15 10:00:00';
const page1000 = await db.table('events')
  .where('created_at', 'lt', lastTimestamp)
  .orderBy('created_at', 'DESC')
  .limit(20)
  .execute();

Examples

Leaderboard

const leaderboard = await db.table('game_scores')
  .select(['user_id'])
  .sum('score', 'total_score')
  .count('id', 'games_played')
  .groupBy(['user_id'])
  .orderBy('total_score', 'DESC')
  .limit(100)
  .execute();

Recent Activity Feed

const activityFeed = await db.table('events')
  .select(['id', 'type', 'user_id', 'created_at'])
  .orderBy('created_at', 'DESC')
  .limit(50)
  .execute();

Browse Products with Pagination

async function browseProducts(category: string, page: number) {
  const pageSize = 24;
  const offset = (page - 1) * pageSize;

  return await db.table('products')
    .select(['id', 'name', 'price', 'image_url'])
    .where('category', 'eq', category)
    .where('in_stock', 'eq', true)
    .orderBy('price', 'ASC')
    .limit(pageSize)
    .offset(offset)
    .execute();
}

Unique Email Domains

const domains = await db.table('users')
  .select([selectExpr('splitByChar(\\'@\\', email)[2]', 'domain')])
  .distinct()
  .orderBy('domain', 'ASC')
  .execute();

On this page