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

📘 How It Works

Learn how compilation gives you safety and performance.

Read →

🔄 N+1 Elimination

The root cause of unpredictable performance.

Read →

🔍 Why Compilation

Why compiled queries beat interpreted ones.

Read →

Build Predictable Systems

Stop chasing performance issues. Build it right from the start.