Querying Data

Viewing:

Moose provides type-safe SQL querying for your OlapTable and MaterializedView instances. Use cases include:

  • Building APIs to expose your data to client/frontend applications
  • Building transformation pipelines inside your database with materialized views

Querying with MooseClient

Use MooseClient to query data from existing tables and materialized views.

Basic Querying

BasicQuerying.ts
import { MooseClient, sql } from "@514labs/moose-lib";
import { UserTable } from "./tables/UserTable";
 
const client = new MooseClient();
 
// Query existing table
const query = sql`
  SELECT id, name, email 
  FROM ${UserTable}
  WHERE status = 'active'
  LIMIT 10
`;
 
const result = await client.query.execute(query);
const data = await result.json();

Querying Materialized Views

QueryMaterializedView.ts
import { MooseClient, sql } from "@514labs/moose-lib";
 
const client = new MooseClient();
 
// Query existing materialized view
const query = sql`
  SELECT user_id, total_orders, average_order_value
  FROM user_stats_view
  WHERE total_orders > 10
  ORDER BY average_order_value DESC
`;
 
const result = await client.query.execute(query);

Select With Column and Table References

TypedReferences.ts
import { sql } from "@514labs/moose-lib";
import { UserTable, OrderTable } from "./tables";
 
// Reference table columns with type safety
const cols = UserTable.columns;
const query = sql`
  SELECT 
    ${cols.id},
    ${cols.name},
    ${cols.email}
  FROM ${UserTable}
  WHERE ${cols.status} = 'active'
`;
 
// Multiple table references
const joinQuery = sql`
  SELECT 
    ${UserTable.columns.id},
    ${UserTable.columns.name},
    ${OrderTable.columns.order_value}
  FROM ${UserTable}
  JOIN ${OrderTable} ON ${UserTable.columns.id} = ${OrderTable.columns.user_id}
`;

When you query a materialized view, you reference the MaterializedView.targetTable to get the columns of the target table.

TypedReferences.ts
import { sql } from "@514labs/moose-lib";
import { ExampleMaterializedView } from "./materialized-views";
 
 
const query = sql`
  SELECT 
    ${ExampleMaterializedView.targetTable.columns.id},
    ${ExampleMaterializedView.targetTable.columns.name},
    ${ExampleMaterializedView.targetTable.columns.email}
  FROM ${ExampleMaterializedView.targetTable}
`;

In ClickHouse, when you query a Materialized View that has columns of type AggregateFunction in the result set, ordinarily you would need to run:

SELECT sumMerge(amount) FROM {ExampleMaterializedView}

When querying this with Moose, you can just reference the column name in the sql template literal. The interpolation will be replaced with the correct ClickHouse function:

TypedReferences.ts
import { sql } from "@514labs/moose-lib";
import { ExampleMaterializedView } from "./materialized-views";
 
const query = sql`
  SELECT ${ExampleMaterializedView.targetTable.columns.amount}
  FROM ${ExampleMaterializedView.targetTable}
`;
 
// This will be replaced with:
// SELECT sumMerge(amount) FROM {ExampleMaterializedView}

Filtering with WHERE Clauses

WhereClauses.ts
import { sql } from "@514labs/moose-lib";
 
// Multiple WHERE conditions
const filterQuery = sql`
  SELECT ${UserTable.columns.id}, ${UserTable.columns.name}
  FROM ${UserTable}
  WHERE ${UserTable.columns.status} = 'active'
    AND ${UserTable.columns.created_at} > '2024-01-01'
    AND ${UserTable.columns.email} ILIKE ${'%' + searchTerm + '%'}
`;
 
// Using IN clauses
const inQuery = sql`
  SELECT * FROM ${UserTable}
  WHERE ${UserTable.columns.id} IN (${userIds})
`;
 
// Using BETWEEN
const rangeQuery = sql`
  SELECT * FROM ${UserTable}
  WHERE ${UserTable.columns.age} BETWEEN ${minAge} AND ${maxAge}
`;

Dynamic Query Building

DynamicQueries.ts
import { sql, ConsumptionHelpers as CH } from "@514labs/moose-lib";
 
interface QueryParams {
  sortBy: string;
  fields: string[];
  filters: Record<string, any>;
}
 
const buildDynamicQuery = ({ sortBy, fields, filters }: QueryParams) => {
  // Build field list
  const fieldList = fields.map(field => sql`${CH.column(field)}`);
  
  // Build WHERE conditions
  const conditions = Object.entries(filters)
    .filter(([_, value]) => value !== undefined)
    .map(([key, value]) => sql`${CH.column(key)} = ${value}`);
  
  let query = sql`
    SELECT ${fieldList.join(', ')}
    FROM ${UserTable}
  `;
  
  if (conditions.length > 0) {
    query = sql`${query} WHERE ${conditions.join(' AND ')}`;
  }
  
  query = sql`${query} ORDER BY ${CH.column(sortBy)}`;
  
  return query;
};

Interpolation and Security

Use the sql template literal to build safe queries:

SqlTemplateLiterals.ts
import { sql } from "@514labs/moose-lib";
 
// Safe interpolation with sql template literal
const status = 'active';
const limit = 10;
 
const query = sql`
  SELECT id, name, email
  FROM ${UserTable}
  WHERE ${UserTable.columns.status} = ${status}
  LIMIT ${limit}
`;
 
// Conditional WHERE clauses
interface FilterParams {
  minAge?: number;
  status?: "active" | "inactive";
  searchText?: string;
}
 
const buildConditionalQuery = (filters: FilterParams) => {
  let conditions = [];
  
  if (filters.minAge !== undefined) {
    conditions.push(sql`age >= ${filters.minAge}`);
  }
  
  if (filters.status) {
    conditions.push(sql`status = ${filters.status}`);
  }
  
  if (filters.searchText) {
    conditions.push(sql`(name ILIKE ${'%' + filters.searchText + '%'} OR email ILIKE ${'%' + filters.searchText + '%'})`);
  }
  
  let query = sql`SELECT * FROM ${UserTable}`;
  
  if (conditions.length > 0) {
    query = sql`${query} WHERE ${conditions.join(' AND ')}`;
  }
  
  return sql`${query} ORDER BY created_at DESC`;
};

Common Query Patterns

Sorting and Pagination

SortingPagination.ts
import { sql } from "@514labs/moose-lib";
 
// Multi-column sorting
const sortQuery = sql`
  SELECT ${UserTable.columns.id}, ${UserTable.columns.name}, ${UserTable.columns.created_at}
  FROM ${UserTable}
  ORDER BY ${UserTable.columns.status} ASC, ${UserTable.columns.created_at} DESC
  LIMIT ${limit} OFFSET ${offset}
`;
 
// Dynamic sorting
const dynamicSortQuery = sql`
  SELECT * FROM ${UserTable}
  ORDER BY ${CH.column(sortBy)} ${sortDirection}
  LIMIT ${limit}
`;

Aggregations and Grouping

Aggregations.ts
import { sql } from "@514labs/moose-lib";
 
// Basic aggregations
const aggQuery = sql`
  SELECT 
    ${UserTable.columns.status},
    COUNT(*) as user_count,
    AVG(${UserTable.columns.age}) as avg_age,
    SUM(${UserTable.columns.score}) as total_score
  FROM ${UserTable}
  GROUP BY ${UserTable.columns.status}
  HAVING COUNT(*) > 10
`;
 
// Complex aggregations with CASE statements
const complexAggQuery = sql`
  SELECT 
    ${UserTable.columns.status},
    COUNT(*) as total_users,
    COUNT(CASE WHEN ${UserTable.columns.age} > 25 THEN 1 END) as users_over_25,
    AVG(CASE WHEN ${UserTable.columns.status} = 'active' THEN ${UserTable.columns.score} END) as avg_active_score
  FROM ${UserTable}
  GROUP BY ${UserTable.columns.status}
`;

Joins

Joins.ts
import { sql } from "@514labs/moose-lib";
import { UserTable, OrderTable } from "./tables";
 
// INNER JOIN
const innerJoinQuery = sql`
  SELECT 
    ${UserTable.columns.id},
    ${UserTable.columns.name},
    ${OrderTable.columns.order_value}
  FROM ${UserTable}
  INNER JOIN ${OrderTable} ON ${UserTable.columns.id} = ${OrderTable.columns.user_id}
  WHERE ${OrderTable.columns.status} = 'completed'
`;
 
// LEFT JOIN
const leftJoinQuery = sql`
  SELECT 
    ${UserTable.columns.id},
    ${UserTable.columns.name},
    COUNT(${OrderTable.columns.id}) as order_count
  FROM ${UserTable}
  LEFT JOIN ${OrderTable} ON ${UserTable.columns.id} = ${OrderTable.columns.user_id}
  GROUP BY ${UserTable.columns.id}, ${UserTable.columns.name}
`;

Building APIs with ConsumptionApi

Basic API

BasicAPI.ts
import { ConsumptionApi, sql } from "@514labs/moose-lib";
 
interface QueryParams {
  limit?: number;
  status?: "active" | "inactive";
}
 
interface UserData {
  id: string;
  name: string;
  email: string;
}
 
const userApi = new ConsumptionApi<QueryParams, UserData[]>(
  "users",
  async ({ limit = 10, status }, { client, sql }) => {
    let query = sql`
      SELECT ${UserTable.columns.id}, ${UserTable.columns.name}, ${UserTable.columns.email}
      FROM ${UserTable}
    `;
    
    if (status) {
      query = sql`${query} WHERE ${UserTable.columns.status} = ${status}`;
    }
    
    query = sql`${query} LIMIT ${limit}`;
    
    const result = await client.query.execute<UserData>(query);
    return await result.json();
  }
);

Advanced API with Dynamic Filters

AdvancedAPI.ts
import { ConsumptionApi, sql, ConsumptionHelpers as CH } from "@514labs/moose-lib";
 
interface AdvancedQueryParams {
  filters: Record<string, any>;
  sortBy: string;
  sortDirection: "ASC" | "DESC";
  page: number;
  limit: number;
}
 
const advancedUserApi = new ConsumptionApi<AdvancedQueryParams, UserData[]>(
  "users/advanced",
  async ({ filters, sortBy, sortDirection, page, limit }, { client, sql }) => {
    // Build WHERE conditions
    const conditions = Object.entries(filters)
      .filter(([_, value]) => value !== undefined)
      .map(([key, value]) => sql`${CH.column(key)} = ${value}`);
    
    let query = sql`
      SELECT ${UserTable.columns.id}, ${UserTable.columns.name}, ${UserTable.columns.email}
      FROM ${UserTable}
    `;
    
    if (conditions.length > 0) {
      query = sql`${query} WHERE ${conditions.join(' AND ')}`;
    }
    
    const offset = (page - 1) * limit;
    query = sql`${query} ORDER BY ${CH.column(sortBy)} ${sortDirection} LIMIT ${limit} OFFSET ${offset}`;
    
    const result = await client.query.execute<UserData>(query);
    return await result.json();
  }
);

Common Pitfalls

Common TypeScript Issues

  • Column name typos: Use UserTable.columns.columnName for autocomplete
  • Type mismatches: Ensure your schema types match ClickHouse types
  • Missing imports: Import your table definitions before using them
  • Template literal syntax: Use backticks sql not regular strings
  • Forgetting await: Always await client.query.execute()

Performance Optimization

If your query is slower than expected, there are a few things you can check:

  • If using filters, try to filter on a column that is defined in the orderByFields of the table
  • For common queries, consider creating a materialized view to pre-compute the result set

Further Reading