Predictable Performance by Design
No more surprise slow queries. Compile-time validation catches issues before production.
The Performance Problem
Traditional GraphQL: Runtime Surprises
# Your code looks fine
@resolver
async def posts(parent: User, info) -> list[Post]:
return db.find_posts(user_id=parent.id)
# But if called inside a list of users, it's N+1
@resolver
async def users() -> list[User]:
return db.find_all_users() # 1 query
# ^ Each user loops through posts resolver = 1 + N queries
# You only discover this in production
# When your API slows to a crawl 1000ms
Query time
production peak
Mystery
Root cause
field resolver cascade
Hours
Debug time
tracing query cascade
FraiseQL Solution: Compile-Time Safety
Key Principle: Views Are Compiled, Not Interpreted
When you deploy with FraiseQL:
- Views compile to SQL at deploy time
- The database validates the execution plan
- EXPLAIN ANALYZE checks performance before production
- Your types match your views (compile-time verification)
Step 1: Define View with Single Query
-- Compile once, never worry about N+1
CREATE VIEW v_user_with_posts AS
SELECT
jsonb_build_object(
'id', u.id::text,
'name', u.name,
'posts', (
SELECT jsonb_agg(jsonb_build_object(
'id', p.id::text,
'title', p.title
))
FROM tb_post p
WHERE p.user_id = u.id
)
) AS data
FROM tb_user u;
-- At deploy time, the database creates an index plan for this query
-- EXPLAIN ANALYZE shows it's one query, ~100ms for 1000 users Step 2: Index for Performance
-- FraiseQL helps you identify the right indexes
CREATE INDEX idx_post_user_id ON tb_post(user_id);
CREATE INDEX idx_post_created_at ON tb_post(created_at DESC);
-- Deploy these with your view
-- Database optimizer uses them automatically Step 3: Query with Confidence
@fraiseql.query(sql_source="v_user_with_posts")
def users(limit: int = 100) -> list[User]:
# This is always 1 query, always fast
pass
# Performance: Always ~100-200ms regardless of data size
# (as long as indexes are in place) Before & After
❌ Traditional GraphQL
1000-2000ms
Response time (peak)
- Runtime query interpretation
- Field resolvers execute silently
- N+1 discovered in production
- Hours to debug and fix
- Unpredictable performance
- DataLoader complexity
✅ FraiseQL
100-200ms
Response time (consistent)
- Compile-time query verification
- Single query, verified at deploy
- No N+1 possible
- Errors caught before production
- Predictable performance
- No batching complexity
FraiseQL Performance Tools
1. EXPLAIN ANALYZE Integration
# FraiseQL CLI includes performance checker
$ fraiseql explain views/v_user_with_posts.sql
Query Plan:
Seq Scan on tb_user (cost=0.00..1500.00 rows=1000)
Filter: (deleted_at IS NULL)
SubPlan 1
Index Scan on tb_post (cost=0.10..2.50 rows=5)
Index Cond: (user_id = u.id)
Execution time: ~150ms for 1000 users
✅ PASS: Query is optimized 2. Type Validation
# Your Python types must match your views
# Compile-time mismatch detection
@fraiseql.type
class Post:
id: ID
title: str
# Compiler checks this matches v_user_with_posts
# If you change the view, you get a compile error
# before deploying to production 3. Query Logging
# Every query logged with timing
# Not hidden like field resolver cascades
2024-01-15 10:23:45.123 [db] users() executed in 145ms
Query: SELECT ... FROM v_user_with_posts
Rows: 1000
2024-01-15 10:23:46.234 [db] user(id=123) executed in 12ms
Query: SELECT ... FROM v_user_with_posts WHERE id = $1
Rows: 1 Performance Best Practices
1. Create Appropriate Indexes
-- Index foreign keys
CREATE INDEX idx_post_user_id ON tb_post(user_id);
-- Index sorted queries
CREATE INDEX idx_post_created_at ON tb_post(created_at DESC);
-- Composite indexes for common filters
CREATE INDEX idx_post_user_published ON tb_post(user_id, published_at); 2. Limit Result Sets
-- Don't return 10,000 records by default
CREATE VIEW v_recent_posts AS
SELECT ... FROM tb_post
ORDER BY created_at DESC
LIMIT 100; -- Reasonable default
-- Pagination for large result sets
@fraiseql.query(sql_source="v_recent_posts")
def posts(limit: int = 50, offset: int = 0) -> list[Post]:
pass 3. Profile at Decode Time
-- Test with realistic data sizes
CREATE VIEW v_product_recommendations AS
SELECT ... FROM tb_product
WHERE ...
LIMIT 1000;
-- Run EXPLAIN ANALYZE with production-like data
EXPLAIN ANALYZE
SELECT * FROM v_product_recommendations;
-- FraiseQL alerts you if it's slow
-- Fix before deploying 4. Monitor in Production
# FraiseQL includes performance monitoring
# Slow queries trigger alerts
fraiseql_config:
performance:
slow_query_threshold_ms: 200
alert_on_slow: true
# Dashboard shows query times and trends
# Alerts fire before users complain Real-World Performance Gains
Latency Improvement
Typical reduction from DataLoader patterns
5-10x faster
Predictability
Standard deviation of query times
80% reduction
CPU Usage
Fewer queries = less CPU per request
40-60% lower
P99 Latency
Worst-case response time improvement
10-100x better
Related Resources
Build Predictable Systems
Stop chasing performance issues. Build it right from the start.