Advanced Queries
The basic query builder covers most needs — where, order_by, limit, offset. But sometimes you need aggregations, subqueries, common table expressions (CTEs), or window functions. Kura supports all of these.
Aggregates
%% Count all published posts
Q = kura_query:from(post),
Q1 = kura_query:where(Q, {status, published}),
Q2 = kura_query:select(Q1, [{count, id}]),
{ok, [#{count => Count}]} = blog_repo:all(Q2).
%% Multiple aggregates
Q = kura_query:from(post),
Q1 = kura_query:group_by(Q, [user_id]),
Q2 = kura_query:select(Q1, [user_id, {count, id}, {max, inserted_at}]),
{ok, Stats} = blog_repo:all(Q2).
%% [#{user_id => 1, count => 5, max => {{2026,2,23},{12,0,0}}}, ...]
Supported aggregate functions: count, sum, avg, min, max.
Having clauses
Filter grouped results:
%% Users with more than 10 posts
Q = kura_query:from(post),
Q1 = kura_query:group_by(Q, [user_id]),
Q2 = kura_query:select(Q1, [user_id, {count, id}]),
Q3 = kura_query:having(Q2, {count, id, '>', 10}),
{ok, ActiveAuthors} = blog_repo:all(Q3).
Joins
%% Join posts with users
Q = kura_query:from(post),
Q1 = kura_query:join(Q, user, {post, user_id, user, id}),
Q2 = kura_query:select(Q1, [{post, [id, title]}, {user, [username]}]),
{ok, Results} = blog_repo:all(Q2).
%% Left join (include posts without comments)
Q = kura_query:from(post),
Q1 = kura_query:left_join(Q, comment, {post, id, comment, post_id}),
Q2 = kura_query:group_by(Q1, [{post, id}]),
Q3 = kura_query:select(Q2, [{post, [id, title]}, {count, {comment, id}}]),
{ok, PostsWithCounts} = blog_repo:all(Q3).
Subqueries
Use a query as a condition in another query:
%% Posts by users who joined in the last 30 days
RecentUsers = kura_query:from(user),
RecentUsers1 = kura_query:where(RecentUsers, {inserted_at, '>=', ThirtyDaysAgo}),
RecentUsers2 = kura_query:select(RecentUsers1, [id]),
Q = kura_query:from(post),
Q1 = kura_query:where(Q, {user_id, in, {subquery, RecentUsers2}}),
{ok, Posts} = blog_repo:all(Q1).
Common Table Expressions (CTEs)
CTEs make complex queries readable by breaking them into named steps:
%% Find the top 5 authors and their latest post
TopAuthors = kura_query:from(post),
TopAuthors1 = kura_query:group_by(TopAuthors, [user_id]),
TopAuthors2 = kura_query:select(TopAuthors1, [user_id, {count, id}]),
TopAuthors3 = kura_query:order_by(TopAuthors2, [{count, desc}]),
TopAuthors4 = kura_query:limit(TopAuthors3, 5),
Q = kura_query:with(<<"top_authors">>, TopAuthors4),
Q1 = kura_query:from_cte(Q, <<"top_authors">>),
Q2 = kura_query:join(Q1, user, {<<"top_authors">>, user_id, user, id}),
{ok, Results} = blog_repo:all(Q2).
Window functions
Compute values across a set of rows without collapsing them:
%% Rank posts by comment count within each user
Q = kura_query:from(post),
Q1 = kura_query:left_join(Q, comment, {post, id, comment, post_id}),
Q2 = kura_query:select(Q1, [
{post, [id, title, user_id]},
{count, {comment, id}},
{window, row_number, [], [{partition_by, {post, user_id}},
{order_by, [{count, desc}]}]}
]),
{ok, RankedPosts} = blog_repo:all(Q2).
Union queries
Combine results from multiple queries:
Drafts = kura_query:from(post),
Drafts1 = kura_query:where(Drafts, {status, draft}),
Drafts2 = kura_query:select(Drafts1, [id, title, status]),
Archived = kura_query:from(post),
Archived1 = kura_query:where(Archived, {status, archived}),
Archived2 = kura_query:select(Archived1, [id, title, status]),
Q = kura_query:union(Drafts2, Archived2),
{ok, Results} = blog_repo:all(Q).
Distinct
Q = kura_query:from(post),
Q1 = kura_query:select(Q, [user_id]),
Q2 = kura_query:distinct(Q1),
{ok, UniqueAuthors} = blog_repo:all(Q2).
Raw SQL escape hatch
When the query builder doesn't cover your case, use raw SQL:
SQL = "SELECT p.id, p.title, COUNT(c.id) as comment_count "
"FROM posts p LEFT JOIN comments c ON c.post_id = p.id "
"GROUP BY p.id ORDER BY comment_count DESC LIMIT $1",
{ok, Results} = blog_repo:query(SQL, [10]).
query/2 returns rows as maps with atom keys.
Next, let's cover transactions and multi for atomic multi-step operations.