CQRS Pattern
FraiseQL uses Command Query Responsibility Segregation (CQRS) at the database level. You separate read and write operations into distinct structures that you design and own: normalized tables (tb_*) for writes, JSONB views (v_*) for reads.
Why CQRS?
Section titled “Why CQRS?”Traditional APIs couple reads and writes to the same tables. A single tb_user table must serve both fast reads and safe writes — and it does neither well:
The problem: one table serves conflicting needs
graph LR API[API Layer] --> T[(tb_user table)] T --> R[Read: SELECT *] T --> W[Write: INSERT/UPDATE] R -.->|conflicts| WFraiseQL separates these concerns at compile time. Write tables handle normalization and constraints. Read views handle denormalization and speed. An auto-sync bridge keeps them consistent:
The solution: separated at compile time
graph LR W[Write Path] --> TB[(tb_* Tables)] TB --> S[sync_tv_*()] S --> TV[(tv_* Projections)] TV --> V[v_* Views] V --> R[Read Path]What this separation buys you
Section titled “What this separation buys you”- Adding a GraphQL field = add a column to the view. No migration needed, no type regeneration.
- Renaming a field = rename in the view, not in the table. No data change, no FK cascade.
- A slow query = add an index or rewrite the view. The API surface stays stable.
- A FraiseQL upgrade = recompile. Your SQL doesn’t change.
- A table schema change = invisible to API clients, who only see what the view exposes.
The cost: you write views. The payoff: the API is decoupled from the storage schema, and you can evolve both independently.
CQRS Separation
Section titled “CQRS Separation”Here is the full picture — normalized tables on the write side, denormalized JSONB views on the read side, connected by an auto-sync bridge:
graph LR M[GraphQL Mutation] --> FN[fn_* Function] FN --> TB[(tb_* Write Tables)] TB --> SYNC[sync_tv_*()] SYNC --> TV[(tv_* Materialized)] TV --> V[v_* Read Views] V --> Q[GraphQL Query]Naming Conventions
Section titled “Naming Conventions”FraiseQL expects strict prefixes to identify each layer:
Write Side (Tables)
Section titled “Write Side (Tables)”| Prefix | Purpose | Example |
|---|---|---|
tb_ | Base table for writes | tb_user, tb_post, tb_order |
-- Write table: normalized, constrainedCREATE TABLE tb_user ( -- Trinity Identifiers pk_user BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL, identifier TEXT UNIQUE NOT NULL,
-- Normalized data email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, bio TEXT,
-- Metadata created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());-- Write table: normalized, constrainedCREATE TABLE tb_user ( -- Trinity Identifiers pk_user BIGINT PRIMARY KEY AUTO_INCREMENT, id CHAR(36) NOT NULL UNIQUE DEFAULT (UUID()), identifier VARCHAR(255) UNIQUE NOT NULL,
-- Normalized data email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, bio TEXT,
-- Metadata created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP);-- Write table: normalized, constrainedCREATE TABLE tb_user ( -- Trinity Identifiers pk_user INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, identifier TEXT UNIQUE NOT NULL,
-- Normalized data email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, bio TEXT,
-- Metadata created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);-- Write table: normalized, constrainedCREATE TABLE dbo.tb_user ( -- Trinity Identifiers pk_user BIGINT PRIMARY KEY IDENTITY(1,1), id UNIQUEIDENTIFIER NOT NULL UNIQUE DEFAULT NEWSEQUENTIALID(), identifier NVARCHAR(255) UNIQUE NOT NULL,
-- Normalized data email NVARCHAR(255) NOT NULL UNIQUE, name NVARCHAR(255) NOT NULL, bio NVARCHAR(MAX),
-- Metadata created_at DATETIMEOFFSET DEFAULT SYSDATETIMEOFFSET(), updated_at DATETIMEOFFSET DEFAULT SYSDATETIMEOFFSET());Read Side (Views)
Section titled “Read Side (Views)”| Prefix | Purpose | Example |
|---|---|---|
v_ | View for reads | v_user, v_post |
tv_ | Table-backed view (materialized) | tv_user_profile |
-- Read view: denormalized, JSONB responseCREATE OR REPLACE VIEW v_user ASSELECT u.id, jsonb_build_object( 'id', u.id::text, 'identifier', u.identifier, 'email', u.email, 'name', u.name, 'bio', u.bio, 'created_at', u.created_at, 'updated_at', u.updated_at ) AS dataFROM tb_user u;-- Read view: denormalized, JSON responseCREATE OR REPLACE VIEW v_user ASSELECT u.id, JSON_OBJECT( 'id', u.id, 'identifier', u.identifier, 'email', u.email, 'name', u.name, 'bio', u.bio, 'created_at', u.created_at, 'updated_at', u.updated_at ) AS dataFROM tb_user u;-- Read view: denormalized, JSON responseCREATE VIEW v_user ASSELECT u.id, json_object( 'id', u.id, 'identifier', u.identifier, 'email', u.email, 'name', u.name, 'bio', u.bio, 'created_at', u.created_at, 'updated_at', u.updated_at ) AS dataFROM tb_user u;-- Read view: denormalized, JSON responseCREATE VIEW dbo.v_userWITH SCHEMABINDING ASSELECT u.id, ( SELECT u.id, u.identifier, u.email, u.name, u.bio, u.created_at, u.updated_at FOR JSON PATH, WITHOUT_ARRAY_WRAPPER ) AS dataFROM dbo.tb_user u;Materialized Read Tables
Section titled “Materialized Read Tables”For high-traffic reads, use table-backed views (tv_):
-- Materialized projection tableCREATE TABLE tv_user ( id UUID PRIMARY KEY, data JSONB);
-- Sync functionCREATE OR REPLACE FUNCTION sync_tv_user() RETURNS VOID AS $$BEGIN DELETE FROM tv_user; INSERT INTO tv_user (id, data) SELECT id, data FROM v_user;END;$$ LANGUAGE plpgsql;-- Materialized projection tableCREATE TABLE tv_user ( id CHAR(36) PRIMARY KEY, data JSON);
-- Sync procedureDELIMITER //CREATE PROCEDURE sync_tv_user()BEGIN DELETE FROM tv_user; INSERT INTO tv_user (id, data) SELECT id, data FROM v_user;END//DELIMITER ;-- Materialized projection tableCREATE TABLE tv_user ( id TEXT PRIMARY KEY, data TEXT);
-- Sync trigger helperCREATE TRIGGER sync_tv_user_after_insertAFTER INSERT ON tb_userBEGIN DELETE FROM tv_user; INSERT INTO tv_user (id, data) SELECT id, data FROM v_user;END;-- Materialized projection tableCREATE TABLE dbo.tv_user ( id UNIQUEIDENTIFIER PRIMARY KEY, data NVARCHAR(MAX));
-- Sync procedureCREATE PROCEDURE dbo.sync_tv_userASBEGIN DELETE FROM dbo.tv_user; INSERT INTO dbo.tv_user (id, data) SELECT id, data FROM dbo.v_user;END;The Data Column Pattern
Section titled “The Data Column Pattern”Every read view exposes a data column containing the complete GraphQL response:
SELECT data FROM v_user WHERE id = '...';SELECT data FROM v_user WHERE id = '...';SELECT data FROM v_user WHERE id = '...';SELECT data FROM dbo.v_user WHERE id = '...';Returns:
{ "id": "550e8400-e29b-41d4-a716-446655440000", "identifier": "john.doe@example.com", "email": "john.doe@example.com", "name": "John Doe", "bio": "Software engineer", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z"}Why JSONB?
- Single column contains complete response
- No runtime JSON serialization needed
- PostgreSQL optimizes JSONB operations
- Nested objects compose naturally
Trinity Identifier Pattern
Section titled “Trinity Identifier Pattern”Each write table uses three identifier columns:
| Column | Type | Purpose |
|---|---|---|
pk_ | BIGINT | Internal primary key (never exposed) |
id | UUID | External identifier (exposed in API) |
identifier | TEXT | Human-readable slug/code |
CREATE TABLE tb_post ( -- Trinity Identifiers pk_post INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL, identifier TEXT UNIQUE NOT NULL,
-- Foreign key uses internal pk fk_user INTEGER NOT NULL REFERENCES tb_user(pk_user),
-- Post data...);CREATE TABLE tb_post ( -- Trinity Identifiers pk_post BIGINT PRIMARY KEY AUTO_INCREMENT, id CHAR(36) NOT NULL UNIQUE DEFAULT (UUID()), identifier VARCHAR(255) UNIQUE NOT NULL,
-- Foreign key uses internal pk fk_user BIGINT NOT NULL REFERENCES tb_user(pk_user),
-- Post data...);CREATE TABLE tb_post ( -- Trinity Identifiers pk_post INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, identifier TEXT UNIQUE NOT NULL,
-- Foreign key uses internal pk fk_user INTEGER NOT NULL REFERENCES tb_user(pk_user),
-- Post data...);CREATE TABLE dbo.tb_post ( -- Trinity Identifiers pk_post BIGINT PRIMARY KEY IDENTITY(1,1), id UNIQUEIDENTIFIER NOT NULL UNIQUE DEFAULT NEWSEQUENTIALID(), identifier NVARCHAR(255) UNIQUE NOT NULL,
-- Foreign key uses internal pk fk_user BIGINT NOT NULL REFERENCES dbo.tb_user(pk_user),
-- Post data...);Why three identifiers?
pk_(Internal): Fast integer JOINs, never changes, never exposedid(External): Secure UUID for API access, no enumeration attacksidentifier(Human): Readable slugs for URLs, user-facing references
Foreign Keys
Section titled “Foreign Keys”Foreign keys always reference the internal pk_ column:
-- Foreign key patternfk_user INTEGER NOT NULL REFERENCES tb_user(pk_user)-- Foreign key patternfk_user BIGINT NOT NULL REFERENCES tb_user(pk_user)-- Foreign key patternfk_user INTEGER NOT NULL REFERENCES tb_user(pk_user)-- Foreign key patternfk_user BIGINT NOT NULL REFERENCES dbo.tb_user(pk_user)Naming convention: fk_{entity} references pk_{entity}
This gives you:
- Fast integer JOINs internally
- UUID security externally
- No exposure of database internals
Write Operations
Section titled “Write Operations”Writes go through SQL functions that:
- Validate input
- Insert/update
tb_tables - Sync
tv_projections
CREATE OR REPLACE FUNCTION create_user( user_email TEXT, user_name TEXT, user_bio TEXT DEFAULT NULL) RETURNS UUID AS $$DECLARE new_user_id UUID;BEGIN -- Validation IF user_email IS NULL OR user_name IS NULL THEN RAISE EXCEPTION 'Email and name are required'; END IF;
-- Check duplicate IF EXISTS (SELECT 1 FROM tb_user WHERE email = user_email) THEN RAISE EXCEPTION 'User with email % already exists', user_email; END IF;
-- Create user INSERT INTO tb_user (email, name, bio, identifier) VALUES (user_email, user_name, user_bio, user_email) RETURNING id INTO new_user_id;
-- Sync projection PERFORM sync_tv_user();
RETURN new_user_id;END;$$ LANGUAGE plpgsql;DELIMITER //CREATE PROCEDURE create_user( IN user_email VARCHAR(255), IN user_name VARCHAR(255), IN user_bio TEXT)BEGIN DECLARE new_user_id CHAR(36);
-- Validation IF user_email IS NULL OR user_name IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Email and name are required'; END IF;
-- Check duplicate IF EXISTS (SELECT 1 FROM tb_user WHERE email = user_email) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'User with email already exists'; END IF;
-- Create user SET new_user_id = UUID(); INSERT INTO tb_user (id, email, name, bio, identifier) VALUES (new_user_id, user_email, user_name, user_bio, user_email);
-- Sync projection CALL sync_tv_user();
SELECT new_user_id;END//DELIMITER ;CREATE TRIGGER create_user_triggerINSTEAD OF INSERT ON v_user_insertBEGIN INSERT INTO tb_user (email, name, bio, identifier) VALUES (NEW.email, NEW.name, NEW.bio, NEW.email);
-- Sync projection DELETE FROM tv_user; INSERT INTO tv_user (id, data) SELECT id, data FROM v_user;END;CREATE PROCEDURE dbo.create_user @user_email NVARCHAR(255), @user_name NVARCHAR(255), @user_bio NVARCHAR(MAX) = NULLASBEGIN DECLARE @new_user_id UNIQUEIDENTIFIER;
-- Validation IF @user_email IS NULL OR @user_name IS NULL BEGIN THROW 50000, 'Email and name are required', 1; END
-- Check duplicate IF EXISTS (SELECT 1 FROM dbo.tb_user WHERE email = @user_email) BEGIN THROW 50000, 'User with email already exists', 1; END
-- Create user SET @new_user_id = NEWID(); INSERT INTO dbo.tb_user (id, email, name, bio, identifier) VALUES (@new_user_id, @user_email, @user_name, @user_bio, @user_email);
-- Sync projection EXEC dbo.sync_tv_user;
SELECT @new_user_id;END;Read Operations
Section titled “Read Operations”Reads query v_ or tv_ views directly:
-- Single entitySELECT data FROM v_user WHERE id = $1;
-- List with paginationSELECT data FROM v_userORDER BY data->>'created_at' DESCLIMIT $1 OFFSET $2;
-- With filteringSELECT data FROM v_userWHERE data->>'is_active' = 'true'LIMIT 10;-- Single entitySELECT data FROM v_user WHERE id = ?;
-- List with paginationSELECT data FROM v_userORDER BY JSON_UNQUOTE(data->'$.created_at') DESCLIMIT ? OFFSET ?;
-- With filteringSELECT data FROM v_userWHERE JSON_UNQUOTE(data->'$.is_active') = 'true'LIMIT 10;-- Single entitySELECT data FROM v_user WHERE id = ?;
-- List with paginationSELECT data FROM v_userORDER BY json_extract(data, '$.created_at') DESCLIMIT ? OFFSET ?;
-- With filteringSELECT data FROM v_userWHERE json_extract(data, '$.is_active') = 'true'LIMIT 10;-- Single entitySELECT data FROM dbo.v_user WHERE id = @id;
-- List with paginationSELECT data FROM dbo.v_userORDER BY JSON_VALUE(data, '$.created_at') DESCOFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY;
-- With filteringSELECT TOP 10 data FROM dbo.v_userWHERE JSON_VALUE(data, '$.is_active') = 'true';Complete Example
Section titled “Complete Example”Here’s the full CQRS structure for a blog post:
-- WRITE: Base tableCREATE TABLE tb_post ( pk_post INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL, identifier TEXT UNIQUE NOT NULL, fk_user INTEGER NOT NULL REFERENCES tb_user(pk_user), title TEXT NOT NULL, content TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, is_published BOOLEAN DEFAULT false, published_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());
-- READ: View with nested authorCREATE OR REPLACE VIEW v_post ASSELECT p.id, jsonb_build_object( 'id', p.id::text, 'identifier', p.identifier, 'title', p.title, 'slug', p.slug, 'content', p.content, 'is_published', p.is_published, 'published_at', p.published_at, 'created_at', p.created_at, 'updated_at', p.updated_at, 'author', vu.data ) AS dataFROM tb_post pJOIN tb_user u ON u.pk_user = p.fk_userJOIN v_user vu ON vu.id = u.id;
-- READ: Materialized projectionCREATE TABLE tv_post ( id UUID PRIMARY KEY, data JSONB);-- WRITE: Base tableCREATE TABLE tb_post ( pk_post BIGINT PRIMARY KEY AUTO_INCREMENT, id CHAR(36) NOT NULL UNIQUE DEFAULT (UUID()), identifier VARCHAR(255) UNIQUE NOT NULL, fk_user BIGINT NOT NULL REFERENCES tb_user(pk_user), title VARCHAR(255) NOT NULL, content LONGTEXT NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, is_published BOOLEAN DEFAULT false, published_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP);
-- READ: View with nested authorCREATE OR REPLACE VIEW v_post ASSELECT p.id, JSON_OBJECT( 'id', p.id, 'identifier', p.identifier, 'title', p.title, 'slug', p.slug, 'content', p.content, 'is_published', p.is_published, 'published_at', p.published_at, 'created_at', p.created_at, 'updated_at', p.updated_at, 'author', vu.data ) AS dataFROM tb_post pJOIN tb_user u ON u.pk_user = p.fk_userJOIN v_user vu ON vu.id = u.id;
-- READ: Materialized projectionCREATE TABLE tv_post ( id CHAR(36) PRIMARY KEY, data JSON);-- WRITE: Base tableCREATE TABLE tb_post ( pk_post INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, identifier TEXT UNIQUE NOT NULL, fk_user INTEGER NOT NULL REFERENCES tb_user(pk_user), title TEXT NOT NULL, content TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, is_published BOOLEAN DEFAULT 0, published_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);
-- READ: View with nested authorCREATE VIEW v_post ASSELECT p.id, json_object( 'id', p.id, 'identifier', p.identifier, 'title', p.title, 'slug', p.slug, 'content', p.content, 'is_published', p.is_published, 'published_at', p.published_at, 'created_at', p.created_at, 'updated_at', p.updated_at, 'author', json(vu.data) ) AS dataFROM tb_post pJOIN tb_user u ON u.pk_user = p.fk_userJOIN v_user vu ON vu.id = u.id;
-- READ: Materialized projectionCREATE TABLE tv_post ( id TEXT PRIMARY KEY, data TEXT);-- WRITE: Base tableCREATE TABLE dbo.tb_post ( pk_post BIGINT PRIMARY KEY IDENTITY(1,1), id UNIQUEIDENTIFIER NOT NULL UNIQUE DEFAULT NEWSEQUENTIALID(), identifier NVARCHAR(255) UNIQUE NOT NULL, fk_user BIGINT NOT NULL REFERENCES dbo.tb_user(pk_user), title NVARCHAR(255) NOT NULL, content NVARCHAR(MAX) NOT NULL, slug NVARCHAR(255) UNIQUE NOT NULL, is_published BIT DEFAULT 0, published_at DATETIMEOFFSET, created_at DATETIMEOFFSET DEFAULT SYSDATETIMEOFFSET(), updated_at DATETIMEOFFSET DEFAULT SYSDATETIMEOFFSET());
-- READ: View with nested authorCREATE VIEW dbo.v_postWITH SCHEMABINDING ASSELECT p.id, ( SELECT p.id, p.identifier, p.title, p.slug, p.content, p.is_published, p.published_at, p.created_at, p.updated_at, (SELECT vu.data) AS author FOR JSON PATH, WITHOUT_ARRAY_WRAPPER ) AS dataFROM dbo.tb_post pJOIN dbo.tb_user u ON u.pk_user = p.fk_userJOIN dbo.v_user vu ON vu.id = u.id;
-- READ: Materialized projectionCREATE TABLE dbo.tv_post ( id UNIQUEIDENTIFIER PRIMARY KEY, data NVARCHAR(MAX));When to Use tv_ Tables
Section titled “When to Use tv_ Tables”Use tv_ (table-backed views) when:
- High read frequency: The view is queried thousands of times per second
- Complex JOINs: The view involves multiple table joins
- Stable data: Data changes infrequently
- Large datasets: The underlying tables have millions of rows
Stick with v_ views when:
- Data changes frequently
- Real-time accuracy is required
- The view is simple (1-2 table JOINs)
Python Integration
Section titled “Python Integration”Define your GraphQL types with decorators that map to your CQRS structure:
import fraiseql
@fraiseql.typeclass User: """Maps to v_user / tv_user""" id: str identifier: str email: str name: str bio: str | None created_at: str updated_at: str
@fraiseql.query(sql_source="v_user")def users(limit: int = 10, offset: int = 0) -> list[User]: """Read from v_user view""" pass
@fraiseql.mutation(sql_source="fn_create_user", operation="CREATE")def create_user(name: str, email: str) -> User: """Write through create_user function""" passVerify It Works
Section titled “Verify It Works”-
Create the write table and read view:
-- PostgreSQL exampleCREATE TABLE tb_test_item (pk_item INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,name TEXT NOT NULL,value INTEGER NOT NULL);CREATE OR REPLACE VIEW v_test_item ASSELECTid,jsonb_build_object('id', id::text,'name', name,'value', value) AS dataFROM tb_test_item; -
Create a write function:
CREATE OR REPLACE FUNCTION fn_create_test_item(p_input jsonb)RETURNS SETOF v_test_itemLANGUAGE plpgsql AS $$DECLAREv_item_id UUID;BEGININSERT INTO tb_test_item (name, value)VALUES (p_input->>'name', (p_input->>'value')::INTEGER)RETURNING id INTO v_item_id;RETURN QUERY SELECT * FROM v_test_item WHERE id = v_item_id;END;$$; -
Test the write path:
Terminal window curl -X POST http://localhost:8080/graphql \-H "Content-Type: application/json" \-d '{"query": "mutation { createTestItem(input: { name: \"Test\", value: 42 }) { id name value } }"}'Expected response:
{"data": {"createTestItem": {"id": "550e8400-e29b-41d4-a716-446655440000","name": "Test","value": 42}}} -
Verify data in write table:
SELECT * FROM tb_test_item;-- Should show the normalized row -
Verify data in read view:
SELECT data FROM v_test_item WHERE id = '550e8400-...';-- Should return JSONB: {"id": "...", "name": "Test", "value": 42} -
Test the read query:
Terminal window curl -X POST http://localhost:8080/graphql \-H "Content-Type: application/json" \-d '{"query": "{ testItems { id name value } }"}'Expected response:
{"data": {"testItems": [{"id": "550e8400-e29b-41d4-a716-446655440000","name": "Test","value": 42}]}}
Troubleshooting
Section titled “Troubleshooting”View Returns No Data
Section titled “View Returns No Data”-
Check view definition:
-- PostgreSQL\d+ v_test_item-- Check the query planEXPLAIN ANALYZE SELECT data FROM v_test_item; -
Verify the data column:
SELECT data FROM v_test_item LIMIT 1;-- Should return valid JSON/JSONB, not NULL
Write Function Fails
Section titled “Write Function Fails”-
Test function directly in SQL:
SELECT * FROM fn_create_test_item('{"name": "Test", "value": 42}'::jsonb); -
Check for constraint violations:
-- Check if UUID already existsSELECT id FROM tb_test_item WHERE id = 'conflicting-uuid';
Performance Issues
Section titled “Performance Issues”-
Check for missing indexes on
tb_*tables:-- For UUID lookupsCREATE INDEX idx_tb_item_id ON tb_test_item(id);-- For foreign keysCREATE INDEX idx_tb_item_fk ON tb_test_item(fk_other); -
Analyze query plans:
EXPLAIN (ANALYZE, BUFFERS)SELECT data FROM v_test_itemWHERE data->>'name' = 'Test';
Next Steps
Section titled “Next Steps”- Developer-Owned SQL — Why you write the SQL
- View Composition — How views embed other views
- 🍯 Confiture — Database lifecycle management
- Mutations — Write operations in detail
- Naming Conventions — Complete naming guide