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
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();You can use a formatted string with execute:
from moose_lib import MooseClient
from app.UserTable import UserTable
client = MooseClient()
status = "active"
limit = 10
query = """
SELECT id, name, email
FROM {table}
WHERE status = {status}
LIMIT {limit}
"""
rows = client.query.execute(query, {"table": UserTable, "status": status, "limit": limit})
rows = client.query.execute(query)This allows you to safely interpolate the table and column names while still using your Moose OlapTables and columns.
Querying Materialized Views
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);You can use a formatted string with execute:
from moose_lib import MooseClient
client = MooseClient()
min_orders = 10
query = """
SELECT user_id, total_orders, average_order_value
FROM user_stats_view
WHERE total_orders > {min_orders}
ORDER BY average_order_value DESC
"""
rows = client.query.execute(query, {"min_orders": min_orders})
Select With Column and Table References
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.
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:
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}from moose_lib import MooseClient
from app.UserTable import UserTable
client = MooseClient()
status = "active"
query = """
SELECT
{column}
FROM {table}
WHERE status = {status}
"""
rows = client.query.execute(query, {"column": UserTable.cols.id, "table": UserTable, "status": status})
Filtering with WHERE Clauses
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}
`;from moose_lib import MooseClient
client = MooseClient()
status = "active"
start_date = "2024-01-01"
search_pattern = "%example%"
min_age = 18
max_age = 65
user_ids = [1, 2, 3, 4, 5]
# Multiple WHERE conditions
filter_query = """
SELECT id, name
FROM {table}
WHERE status = {status}
AND created_at > {start_date}
AND email ILIKE {search_pattern}
"""
# Using BETWEEN
range_query = """
SELECT * FROM {table}
WHERE age BETWEEN {min_age} AND {max_age}
"""
# Using IN
in_query = """
SELECT * FROM {table}
WHERE id IN {user_ids}
"""
# Execute examples
filter_rows = client.query.execute(filter_query, {"table": UserTable, "status": status, "startDate": start_date, "searchPattern": search_pattern})
range_rows = client.query.execute(range_query, {"table": UserTable, "minAge": min_age, "maxAge": max_age})
in_rows = client.query.execute(in_query, {"table": UserTable, "userIds": user_ids})Dynamic Query Building
Use the sql template literal to build safe queries:
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`;
};Moose provides two distinct approaches for executing queries in Python. Choose the right one for your use case:
- Option 1: Use formatted strings with
execute - Option 2: Use
execute_rawwith parameter binding (lowest level of abstraction)
from moose_lib import MooseClient
from pydantic import BaseModel, Field, validator
from typing import Optional
client = MooseClient()
# Example: Static query with validated parameters
def get_active_users(status: str, limit: int):
# Static table/column names, validated parameters
query = """
SELECT id, name, email
FROM {table}
WHERE status = {status}
LIMIT {limit}
"""
return client.query.execute(query, {"table": UserTable, "status": status, "limit": limit})
# Usage with validated input
active_users = get_active_users("active", 10)
class UserQueryParams(BaseModel):
status: str = Field(..., pattern=r"^(active|inactive|pending)$")
limit: int = Field(default=10, ge=1, le=1000)
def build_validated_query(params: UserQueryParams):
# All parameters are validated by Pydantic
query = """
SELECT id, name, email
FROM {table}
WHERE status = {status}
LIMIT {limit}
"""
return client.query.execute(query, {"table": UserTable, "status": params.status, "limit": params.limit})Building APIs
To build REST APIs that expose your data, see the Bring Your Own API Framework documentation for comprehensive examples and patterns using Express, Koa, Fastify, or FastAPI.
Common Pitfalls
Common TypeScript Issues
- Column name typos: Use
UserTable.columns.columnNamefor autocomplete - Type mismatches: Ensure your schema types match ClickHouse types
- Missing imports: Import your table definitions before using them
- Template literal syntax: Use backticks
sqlnot 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
orderByFieldsof the table - For common queries, consider creating a materialized view to pre-compute the result set