Read-only collections based on SQL queries for complex data aggregations and joins
View collections are special read-only collections backed by SQL queries. They’re perfect for complex data aggregations, joins, and computed fields without duplicating data.
View collections are read-only. You cannot create, update, or delete records through the API. To modify data, update the underlying source collections.
stats := core.NewViewCollection("user_stats")stats.ViewQuery = ` SELECT users.id, users.email, users.username, COUNT(DISTINCT posts.id) as total_posts, COUNT(DISTINCT comments.id) as total_comments, MAX(posts.created) as last_post_date FROM users LEFT JOIN posts ON posts.author = users.id LEFT JOIN comments ON comments.author = users.id GROUP BY users.id`
The query must include an id column. If your query doesn’t naturally have one, use SQL to create it (e.g., ROW_NUMBER() or alias an existing unique column).
Every view must have an id column for PocketBase to identify records.
-- Using existing IDSELECT users.id, users.name FROM users-- Creating an ID from multiple columnsSELECT (users.id || '_' || posts.id) as id, users.name, posts.titleFROM usersJOIN posts ON posts.author = users.id
2
Use valid SQLite syntax
PocketBase uses SQLite, so your queries must be valid SQLite SQL.
-- Good: SQLite functionsSELECT id, UPPER(name) as name FROM users-- Bad: PostgreSQL-specific syntaxSELECT id, name::text FROM users
3
Reference collections by name
Always use the collection name (not the table name) in your queries.
-- CorrectSELECT * FROM users WHERE status = 'active'-- Also correct with backticksSELECT * FROM `users` WHERE `status` = 'active'
4
Be mindful of performance
Complex queries can impact performance. Add indexes to source collections.
-- This might be slow without indexesSELECT * FROM users uJOIN posts p ON p.author = u.idWHERE p.created > '2024-01-01'
inventory := core.NewViewCollection("product_inventory")inventory.ViewQuery = ` SELECT products.id, products.name, products.sku, products.price, COALESCE(SUM(stock.quantity), 0) as total_stock, COUNT(DISTINCT warehouses.id) as warehouse_count, CASE WHEN COALESCE(SUM(stock.quantity), 0) = 0 THEN 'out_of_stock' WHEN COALESCE(SUM(stock.quantity), 0) < 10 THEN 'low_stock' ELSE 'in_stock' END as stock_status FROM products LEFT JOIN stock ON stock.product = products.id LEFT JOIN warehouses ON warehouses.id = stock.warehouse GROUP BY products.id`inventory.ListRule = core.String("@request.auth.role = 'admin' || @request.auth.role = 'staff'")inventory.ViewRule = core.String("@request.auth.role = 'admin' || @request.auth.role = 'staff'")
-- Flatten nested relations for faster readsSELECT posts.id, posts.title, posts.content, users.username as author_name, users.email as author_email, categories.name as category_nameFROM postsJOIN users ON users.id = posts.authorJOIN categories ON categories.id = posts.category
-- Daily activity summarySELECT DATE(created) as id, DATE(created) as date, COUNT(DISTINCT user_id) as active_users, COUNT(*) as total_events, SUM(CASE WHEN event_type = 'login' THEN 1 ELSE 0 END) as logins, SUM(CASE WHEN event_type = 'signup' THEN 1 ELSE 0 END) as signupsFROM activity_logsGROUP BY DATE(created)