Materialized Views

SurrealEngine provides support for SurrealDB materialized views, allowing you to create pre-computed aggregations and transformations of your data. In SurrealDB 2.x+, materialized views are live and automatically update when underlying data changes.

Creating Materialized Views

Basic Creation

Create a materialized view from a document model:

from surrealengine import Document, StringField, IntField, FloatField
from surrealengine import MaterializedView

class Product(Document):
    name = StringField(required=True)
    category = StringField(required=True)
    price = FloatField(required=True)
    quantity = IntField()

    class Meta:
        collection = "products"

# Create a materialized view
class ProductStats(MaterializedView):
    table_name = "product_stats"

    @classmethod
    def get_source_query(cls):
        return Product.objects.group_by("category")

# Create the view in the database
await ProductStats.create()

Create Options (v0.5.0+)

Control how the materialized view table is created:

# Create with overwrite - replaces existing table
await ProductStats.create(overwrite=True)

# Create only if it doesn't exist
await ProductStats.create(if_not_exists=True)

# Basic create (will fail if table exists)
await ProductStats.create()

These options map to SurrealDB’s DEFINE TABLE clauses:

  • overwrite=TrueDEFINE TABLE OVERWRITE ...

  • if_not_exists=TrueDEFINE TABLE IF NOT EXISTS ...

Advanced View Configuration

Group By with Aggregations (v0.5.0+)

Create views with aggregated data using improved GROUP BY parsing:

class CategoryStats(MaterializedView):
    table_name = "category_stats"

    @classmethod
    def get_source_query(cls):
        return (Product.objects
            .group_by("category")
            .select("category", "COUNT(*) as total", "AVG(price) as avg_price"))

await CategoryStats.create()

Group All (v0.5.0+)

Aggregate across all records without grouping:

class OverallStats(MaterializedView):
    table_name = "overall_stats"

    @classmethod
    def get_source_query(cls):
        return (Product.objects
            .group_by(all=True)
            .select("COUNT(*) as total_products", "SUM(quantity) as total_inventory"))

await OverallStats.create()

Querying Materialized Views

Query materialized views like regular documents:

# Get all stats
stats = await ProductStats.objects.all()

# Filter stats
electronics = await ProductStats.objects.filter(category="electronics")

# Order by aggregated values
top_categories = await ProductStats.objects.order_by("-total_sales").limit(10)

# Use with other query methods
stats = await (ProductStats.objects
    .filter(total_sales__gt=1000)
    .omit("internal_field")
    .timeout("5s")
    .all())

Synchronous Operations

All async operations have synchronous equivalents:

# Create view synchronously
ProductStats.create_sync()
ProductStats.create_sync(overwrite=True)
ProductStats.create_sync(if_not_exists=True)

# Query synchronously
stats = ProductStats.objects.all_sync()
stats = ProductStats.objects.filter(category="electronics").all_sync()

Deprecation Notice

Warning

refresh() and refresh_sync() are deprecated (v0.5.0+)

In SurrealDB 2.x+, materialized views derived from tables are live and automatically update when underlying data changes. Manual refresh is not needed and these methods are now no-ops:

# DEPRECATED - No longer needed
await ProductStats.refresh()
ProductStats.refresh_sync()

These methods will be removed in a future version. Simply query the view to get current data:

# Correct way - views are always up to date
stats = await ProductStats.objects.all()

Live Updates

SurrealDB materialized views are live, meaning they automatically update when:

  • Source table data is inserted

  • Source table data is updated

  • Source table data is deleted

No manual refresh or rebuild is required. Just query the view to get current aggregated data.

Complex Aggregations

Use aggregation pipelines for complex transformations:

from surrealengine import AggregationPipeline
from surrealengine.aggregation import Count, Sum, Avg, Max, Min

class SalesSummary(MaterializedView):
    table_name = "sales_summary"

    @classmethod
    def get_source_query(cls):
        pipeline = AggregationPipeline(Product.objects)
        return (pipeline
            .group(by_fields="category",
                   total_items=Count("*"),
                   total_revenue=Sum("price * quantity"),
                   avg_price=Avg("price"),
                   max_price=Max("price"),
                   min_price=Min("price"))
            .sort(total_revenue="DESC")
            .limit(50))

await SalesSummary.create(if_not_exists=True)

Best Practices

  1. Use if_not_exists for idempotency:

    await ProductStats.create(if_not_exists=True)
    
  2. Choose meaningful view names:

    class UserActivitySummary(MaterializedView):
        table_name = "user_activity_summary"
    
  3. Don’t use refresh() - views are live and always current

  4. Index view columns used in filters:

    class ProductStats(MaterializedView):
        table_name = "product_stats"
    
        class Meta:
            indexes = [
                {"fields": ["category"], "unique": False},
                {"fields": ["total_sales"], "unique": False}
            ]
    
  5. Use GROUP ALL for global aggregations:

    # For global stats across all records
    return Product.objects.group_by(all=True).select(...)
    

Performance Considerations

  • Materialized views improve query performance by pre-computing aggregations

  • Views are stored as regular tables in SurrealDB

  • Live updates may have a small performance impact on write operations

  • For very large datasets, consider partitioning data or using time-based views

  • Use omit() and field selection to minimize data transfer

Migration from Older Versions

If you’re upgrading from versions < 0.5.0:

# OLD (< v0.5.0)
await ProductStats.create()
await ProductStats.refresh()  # Manually refresh

# NEW (v0.5.0+)
await ProductStats.create(if_not_exists=True)
# No refresh needed - views are live!
stats = await ProductStats.objects.all()  # Always current