Skip to main content

GraphQL Query Optimizer for SQLAlchemy - Optimize your GraphQL queries with field selection, caching, and DataLoader support

Project description

GraphQL Query Optimizer for SQLAlchemy

PyPI version Python versions License: MIT Tests

A powerful library for optimizing GraphQL queries with SQLAlchemy. Automatically selects only the requested fields, implements caching, and provides DataLoader for N+1 problem prevention.

Features

  • Field Selection Optimization - Only query columns that are requested in GraphQL
  • Query Caching - Built-in LRU cache with TTL support
  • DataLoader - Batch loading to solve N+1 query problem
  • Async Support - Full async/await support for async SQLAlchemy
  • Multiple Libraries - Works with Strawberry, Graphene, and Ariadne
  • Type Safe - Full type hints for IDE support

Installation

# Basic installation
pip install gql-optimizer

# With Strawberry support
pip install gql-optimizer[strawberry]

# With Graphene support
pip install gql-optimizer[graphene]

# With async support
pip install gql-optimizer[async]

# All extras
pip install gql-optimizer[all]

Quick Start

Basic Usage

from gql_optimizer import QueryOptimizer
import strawberry
from strawberry.types import Info

@strawberry.type
class Query:
    @strawberry.field
    def orders(self, info: Info, limit: int = 10) -> List[Order]:
        session = get_session()
        
        # Create optimizer
        opt = QueryOptimizer(info, OrderModel, session)
        
        # Get optimized results
        return opt.get_many(limit=limit)

What happens:

# GraphQL Query
query {
  orders(limit: 10) {
    id
    orderId
    totalPrice
  }
}
-- Generated SQL (only requested columns!)
SELECT id, order_id, total_price FROM orders LIMIT 10

-- Instead of:
-- SELECT id, order_id, total_price, status, courier_id, 
--        basket_id, created_at, ... (50+ columns) FROM orders

With Caching

@strawberry.field
def orders(self, info: Info, limit: int = 10) -> List[Order]:
    opt = QueryOptimizer(
        info, 
        OrderModel, 
        session,
        cache=True,      # Enable caching
        cache_ttl=60     # Cache for 60 seconds
    )
    
    return opt.get_many(limit=limit)

With DataLoader (N+1 Prevention)

from gql_optimizer import QueryOptimizer, SyncDataLoader

@strawberry.field
def orders_with_details(self, info: Info) -> List[Order]:
    session = get_session()
    loader = SyncDataLoader(session)
    
    # Get orders with courier_id included
    opt = QueryOptimizer(
        info, 
        OrderModel, 
        session,
        always_include=["id", "courier_id", "basket_id"]
    )
    
    orders = opt.get_many(limit=10)
    
    # Batch load related data (single query each!)
    courier_ids = [o.courier_id for o in orders if o.courier_id]
    basket_ids = [o.basket_id for o in orders if o.basket_id]
    
    courier_map = {
        c.id: c for c in loader.load_many(CourierModel, courier_ids)
        if c
    }
    basket_map = {
        b.basket_id: b for b in loader.load_many(
            BasketModel, basket_ids, key_field="basket_id"
        )
        if b
    }
    
    # Attach relationships
    for order in orders:
        order.courier = courier_map.get(order.courier_id)
        order.basket = basket_map.get(order.basket_id)
    
    return orders

Result:

-- Only 3 queries instead of N+1!
SELECT ... FROM orders LIMIT 10
SELECT ... FROM couriers WHERE id IN (...)
SELECT ... FROM baskets WHERE basket_id IN (...)

Async Usage

from gql_optimizer import QueryOptimizer, AsyncDataLoader

@strawberry.field
async def orders(self, info: Info) -> List[Order]:
    async with AsyncSessionLocal() as session:
        opt = QueryOptimizer(info, OrderModel, session, cache=True)
        return await opt.get_many_async(limit=10)

API Reference

QueryOptimizer

QueryOptimizer(
    info,                    # GraphQL info object
    model,                   # SQLAlchemy model class
    session=None,            # Database session (optional if in context)
    always_include=["id"],   # Fields to always include
    cache=False,             # Enable caching
    cache_ttl=60,            # Cache TTL in seconds
    optimize_nested=False    # Optimize nested relationships
)

Methods

Method Description
get_one(filter, as_model=True) Get single record
get_many(filter, order_by, limit, offset) Get multiple records
get_one_async(...) Async version of get_one
get_many_async(...) Async version of get_many
query() Get raw SQLAlchemy Query
count(filter) Count matching records
exists(filter) Check if records exist

SyncDataLoader

loader = SyncDataLoader(session)

# Single load
courier = loader.load(CourierModel, "courier_123")

# Batch load
couriers = loader.load_many(CourierModel, ["c1", "c2", "c3"])

# Custom key field
order = loader.load(OrderModel, "ORD-123", key_field="order_id")

QueryCache

from gql_optimizer import get_cache, clear_cache, configure_cache

# Get global cache
cache = get_cache()

# Clear all cache
clear_cache()

# Configure cache
configure_cache(ttl_seconds=120, max_size=2000)

# Cache stats
stats = cache.stats()
# {'size': 45, 'max_size': 1000, 'hit_rate': 87.5, ...}

Framework Support

Strawberry (Recommended)

import strawberry
from strawberry.types import Info
from gql_optimizer import QueryOptimizer

@strawberry.type
class Query:
    @strawberry.field
    def orders(self, info: Info) -> List[Order]:
        opt = QueryOptimizer(info, OrderModel, session)
        return opt.get_many(limit=10)

Graphene

import graphene
from gql_optimizer import QueryOptimizer

class Query(graphene.ObjectType):
    orders = graphene.List(OrderType)
    
    def resolve_orders(self, info):
        opt = QueryOptimizer(info, OrderModel, session)
        return opt.get_many(limit=10)

Ariadne

from ariadne import QueryType
from gql_optimizer import QueryOptimizer

query = QueryType()

@query.field("orders")
def resolve_orders(_, info):
    opt = QueryOptimizer(info, OrderModel, session)
    return opt.get_many(limit=10)

Performance Comparison

Scenario Without Optimizer With Optimizer
Simple query (10 fields requested) 50 columns fetched 10 columns fetched
N+1 problem (10 orders + courier) 11 queries 2 queries
Repeated query DB hit every time Cache hit (0ms)
Response size ~50KB ~10KB

Best Practices

1. Always Include Foreign Keys

opt = QueryOptimizer(
    info, OrderModel, session,
    always_include=["id", "courier_id", "basket_id"]  # For relationships
)

2. Use Cache for Read-Heavy Endpoints

opt = QueryOptimizer(
    info, OrderModel, session,
    cache=True,
    cache_ttl=30  # Short TTL for frequently changing data
)

3. Create New DataLoader Per Request

@strawberry.field
def orders(self, info: Info) -> List[Order]:
    loader = SyncDataLoader(session)  # New loader per request
    # ... use loader

4. Use Type Hints

from gql_optimizer import QueryOptimizer, SyncDataLoader

def get_orders(info: Info, session: Session) -> List[Order]:
    opt: QueryOptimizer = QueryOptimizer(info, OrderModel, session)
    return opt.get_many(limit=10)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

# Clone the repository
git clone https://github.com/yourusername/graphql-query-optimizer.git
cd graphql-query-optimizer

# Install development dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Format code
black src tests
isort src tests

# Type check
mypy src

License

MIT License - see LICENSE file for details.

Changelog

See CHANGELOG.md for a list of changes.

Credits

Inspired by the need for efficient GraphQL + SQLAlchemy integration in production applications.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gql_optimizer-1.0.0.tar.gz (21.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gql_optimizer-1.0.0-py3-none-any.whl (20.7 kB view details)

Uploaded Python 3

File details

Details for the file gql_optimizer-1.0.0.tar.gz.

File metadata

  • Download URL: gql_optimizer-1.0.0.tar.gz
  • Upload date:
  • Size: 21.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for gql_optimizer-1.0.0.tar.gz
Algorithm Hash digest
SHA256 835d8abeafa75c23ed76d6b889e876161286fd3db32d3579612ea31428c27932
MD5 0e732462c7210980061a385b9e244f73
BLAKE2b-256 d4f666bb58d6f1f9c0c5480a02cfb5b24c23fac9f35358de29e21152fb1da5ab

See more details on using hashes here.

File details

Details for the file gql_optimizer-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: gql_optimizer-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 20.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for gql_optimizer-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c36ea5c4a4dea7304c7a6dc2ae7401432a7232b76a612bcff9ade284db44aed8
MD5 10b48edf2c2f6b9118ea53b66ee20eb4
BLAKE2b-256 464036f2c88e106f3fe14e3e646f6288aca5b1888d14b18946d9a3de69f1b352

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page