This document describes how to use views in SyndrDB, including regular views for query abstraction and materialized views for performance optimization.
SyndrDB supports PostgreSQL-style views with two distinct types:
Regular Views (Virtual Bundles)
Materialized Views (Physical Snapshots)
🎯 Query Abstraction: Encapsulate complex queries behind simple names
⚡ Performance Optimization: Pre-compute expensive aggregations
🔒 Security Layer: Control data access through view definitions
📊 Data Simplification: Hide complexity from end users
♻️ Reusability: Share common queries across applications
Regular views are virtual tables defined by a SELECT statement. When you query a view, SyndrDB rewrites the query to execute the underlying SELECT.
Creates a new regular view.
Syntax:
CREATE VIEW "<VIEW_NAME>" AS <SELECT_STATEMENT>;
Example 1: Simple Filter View
CREATE VIEW "ActiveCustomers" AS
SELECT * FROM "Customers" WHERE "Status" == "Active";
Response:
{
"ResultCount": 1,
"Result": "View 'ActiveCustomers' created successfully",
"ExecutionTimeMS": 15.23
}
Example 2: Aggregation View
CREATE VIEW "CustomerStats" AS
SELECT "CustomerID",
COUNT(*) AS "OrderCount",
SUM("Total") AS "TotalSpent",
AVG("Total") AS "AvgOrderValue"
FROM "Orders"
GROUP BY "CustomerID";
Response:
{
"ResultCount": 1,
"Result": "View 'CustomerStats' created successfully",
"ExecutionTimeMS": 12.45
}
Example 3: JOIN View
CREATE VIEW "OrderDetails" AS
SELECT o."OrderID",
o."OrderDate",
c."CustomerName",
c."Email",
o."Total"
FROM "Orders" AS o
JOIN "Customers" AS c ON o."CustomerID" == c."CustomerID";
Response:
{
"ResultCount": 1,
"Result": "View 'OrderDetails' created successfully",
"ExecutionTimeMS": 18.67
}
Query views just like regular bundles.
Example:
SELECT * FROM "ActiveCustomers" WHERE "Country" == "USA";
Behind the Scenes: SyndrDB rewrites this to:
SELECT * FROM "Customers"
WHERE "Status" == "Active" AND "Country" == "USA";
Response:
{
"ResultCount": 42,
"Result": [
{
"CustomerID": "C1001",
"Name": "John Smith",
"Email": "john@example.com",
"Country": "USA",
"Status": "Active"
},
{
"CustomerID": "C1005",
"Name": "Jane Doe",
"Email": "jane@example.com",
"Country": "USA",
"Status": "Active"
}
],
"ExecutionTimeMS": 8.45
}
| Feature | Regular View | Notes |
|---|---|---|
| Data Storage | None | Only stores definition |
| Data Freshness | Always current | Executes on every query |
| Query Performance | Variable | Depends on underlying query complexity |
| Storage Cost | Minimal | ~1KB per view definition |
| Update Behavior | Read-only | Cannot INSERT/UPDATE/DELETE into views |
| Max Name Length | 128 characters | Enforced at creation |
| Max Definition | 64 KB | Enforced at creation |
| Reserved Prefix | Cannot start with _mv_ |
Reserved for materialized views |
Materialized views store the results of a SELECT query as a physical data bundle, providing fast repeated access to expensive query results.
Creates a new materialized view with an initial snapshot.
Syntax:
CREATE MATERIALIZED VIEW "<VIEW_NAME>" AS <SELECT_STATEMENT>;
Example 1: Daily Sales Aggregation
CREATE MATERIALIZED VIEW "DailySales" AS
SELECT DATE("OrderDate") AS "Date",
COUNT(*) AS "OrderCount",
SUM("Total") AS "TotalRevenue",
AVG("Total") AS "AvgOrderValue"
FROM "Orders"
GROUP BY DATE("OrderDate")
ORDER BY DATE("OrderDate") DESC;
Response:
{
"ResultCount": 1,
"Result": "Materialized view 'DailySales' created successfully with 365 rows",
"ExecutionTimeMS": 142.56
}
Example 2: Top Products
CREATE MATERIALIZED VIEW "TopProducts" AS
SELECT p."ProductID",
p."ProductName",
p."Category",
COUNT(*) AS "OrderCount",
SUM(o."Quantity") AS "TotalQuantitySold",
SUM(o."Amount") AS "TotalRevenue"
FROM "Orders" AS o
JOIN "Products" AS p ON o."ProductID" == p."ProductID"
GROUP BY p."ProductID", p."ProductName", p."Category"
ORDER BY SUM(o."Amount") DESC
LIMIT 100;
Response:
{
"ResultCount": 1,
"Result": "Materialized view 'TopProducts' created successfully with 100 rows",
"ExecutionTimeMS": 523.89
}
Example 3: Customer Lifetime Value
CREATE MATERIALIZED VIEW "CustomerLTV" AS
SELECT c."CustomerID",
c."CustomerName",
c."JoinDate",
COUNT(o."OrderID") AS "TotalOrders",
SUM(o."Total") AS "LifetimeValue",
AVG(o."Total") AS "AvgOrderValue",
MAX(o."OrderDate") AS "LastOrderDate",
F:AGE(MAX(o."OrderDate")) AS "DaysSinceLastOrder"
FROM "Customers" AS c
LEFT JOIN "Orders" AS o ON c."CustomerID" == o."CustomerID"
GROUP BY c."CustomerID", c."CustomerName", c."JoinDate";
Response:
{
"ResultCount": 1,
"Result": "Materialized view 'CustomerLTV' created successfully with 15234 rows",
"ExecutionTimeMS": 1847.23
}
Query materialized views like regular bundles. Results come from the stored snapshot (fast).
Example:
SELECT * FROM "DailySales" WHERE "Date" >= "2024-01-01" ORDER BY "TotalRevenue" DESC LIMIT 10;
Response:
{
"ResultCount": 10,
"Result": [
{
"Date": "2024-03-15",
"OrderCount": 1247,
"TotalRevenue": 523891.45,
"AvgOrderValue": 420.15
},
{
"Date": "2024-02-14",
"OrderCount": 1189,
"TotalRevenue": 498234.67,
"AvgOrderValue": 419.12
}
],
"ExecutionTimeMS": 2.34,
"Warnings": [
"Materialized view 'DailySales' last refreshed 36 hours ago"
]
}
Materialized views show warnings when data becomes stale (default: 48 hours since last refresh).
Example Query After 50 Hours:
SELECT * FROM "TopProducts" LIMIT 5;
Response:
{
"ResultCount": 5,
"Result": [...],
"ExecutionTimeMS": 1.23,
"Warnings": [
"⚠️ Materialized view 'TopProducts' is stale (last refreshed 50 hours ago). Consider running: REFRESH MATERIALIZED VIEW \"TopProducts\";"
]
}
Configuration:
Adjust the stale warning threshold in syndrdb.example.yml:
# Number of hours before showing stale warning (default: 48)
view_stale_warning_hours: 48
Materialized views create a hidden data bundle with the _mv_ prefix.
Example:
"DailySales""_mv_DailySales"Storage Characteristics:
SHOW BUNDLES output.bak backups during refreshDeletes a regular view.
Syntax:
DROP VIEW "<VIEW_NAME>";
Example:
DROP VIEW "ActiveCustomers";
Response:
{
"ResultCount": 1,
"Result": "View 'ActiveCustomers' dropped successfully",
"ExecutionTimeMS": 8.12
}
Error Example:
DROP VIEW "NonExistentView";
Response:
{
"Error": "View 'NonExistentView' does not exist",
"ExecutionTimeMS": 2.34
}
Deletes a materialized view and its data bundle.
Syntax:
DROP MATERIALIZED VIEW "<VIEW_NAME>";
Example:
DROP MATERIALIZED VIEW "DailySales";
Response:
{
"ResultCount": 1,
"Result": "Materialized view 'DailySales' and its data bundle '_mv_DailySales' dropped successfully",
"ExecutionTimeMS": 45.67
}
Important Notes:
DROP VIEW)Manually refreshes a materialized view by re-executing its query.
Syntax:
REFRESH MATERIALIZED VIEW "<VIEW_NAME>";
Example:
REFRESH MATERIALIZED VIEW "DailySales";
Response:
{
"ResultCount": 1,
"Result": "Materialized view 'DailySales' refreshed successfully with 366 rows (previous: 365 rows)",
"ExecutionTimeMS": 156.89
}
Refresh Behavior:
.bak backup of old dataLastRefreshed timestampLock Timeout Example:
REFRESH MATERIALIZED VIEW "CustomerLTV";
Response (if lock timeout):
{
"Error": "Failed to acquire lock for refreshing materialized view 'CustomerLTV': timeout after 60 seconds",
"ExecutionTimeMS": 60012.45
}
Performance Tips:
Lists all views in the current or specified database.
Syntax:
SHOW VIEWS;
SHOW VIEWS FROM "<DATABASE_NAME>";
Example 1: Current Database
SHOW VIEWS;
Response:
{
"ResultCount": 5,
"Result": [
{
"ViewName": "ActiveCustomers",
"Type": "VIEW",
"CreatedAt": "2024-01-15T10:30:00Z",
"CreatedBy": "admin"
},
{
"ViewName": "CustomerStats",
"Type": "VIEW",
"CreatedAt": "2024-01-15T11:00:00Z",
"CreatedBy": "analytics_user"
},
{
"ViewName": "DailySales",
"Type": "MATERIALIZED_VIEW",
"CreatedAt": "2024-01-16T08:00:00Z",
"CreatedBy": "admin",
"LastRefreshed": "2024-01-20T08:00:00Z",
"IsStale": false
},
{
"ViewName": "TopProducts",
"Type": "MATERIALIZED_VIEW",
"CreatedAt": "2024-01-16T09:00:00Z",
"CreatedBy": "admin",
"LastRefreshed": "2024-01-18T08:00:00Z",
"IsStale": true
},
{
"ViewName": "CustomerLTV",
"Type": "MATERIALIZED_VIEW",
"CreatedAt": "2024-01-17T10:00:00Z",
"CreatedBy": "analytics_user",
"LastRefreshed": "2024-01-20T02:00:00Z",
"IsStale": false
}
],
"ExecutionTimeMS": 5.67
}
Example 2: Specific Database
SHOW VIEWS FROM "SalesDB";
Response:
{
"ResultCount": 2,
"Result": [
{
"ViewName": "QuarterlySales",
"Type": "MATERIALIZED_VIEW",
"CreatedAt": "2024-01-10T12:00:00Z",
"CreatedBy": "admin",
"LastRefreshed": "2024-01-20T00:00:00Z",
"IsStale": false
},
{
"ViewName": "RegionalPerformance",
"Type": "VIEW",
"CreatedAt": "2024-01-12T14:30:00Z",
"CreatedBy": "sales_manager"
}
],
"ExecutionTimeMS": 4.23
}
Field Descriptions:
ViewName: Name of the viewType: Either "VIEW" or "MATERIALIZED_VIEW"CreatedAt: ISO 8601 timestamp of creationCreatedBy: Username of creatorLastRefreshed: (Materialized views only) ISO 8601 timestamp of last refreshIsStale: (Materialized views only) True if past stale warning thresholdShows detailed metadata and definition for a specific view.
Syntax:
DESCRIBE VIEW "<VIEW_NAME>";
Example 1: Regular View
DESCRIBE VIEW "ActiveCustomers";
Response:
{
"ResultCount": 1,
"Result": {
"ViewName": "ActiveCustomers",
"DatabaseName": "CustomerDB",
"Type": "VIEW",
"Definition": "SELECT * FROM \"Customers\" WHERE \"Status\" == \"Active\"",
"CreatedAt": "2024-01-15T10:30:00Z",
"CreatedBy": "admin",
"ColumnCount": 8,
"ReferencedBundles": ["Customers"]
},
"ExecutionTimeMS": 3.45
}
Example 2: Materialized View
DESCRIBE VIEW "DailySales";
Response:
{
"ResultCount": 1,
"Result": {
"ViewName": "DailySales",
"DatabaseName": "SalesDB",
"Type": "MATERIALIZED_VIEW",
"Definition": "SELECT DATE(\"OrderDate\") AS \"Date\", COUNT(*) AS \"OrderCount\", SUM(\"Total\") AS \"TotalRevenue\", AVG(\"Total\") AS \"AvgOrderValue\" FROM \"Orders\" GROUP BY DATE(\"OrderDate\") ORDER BY DATE(\"OrderDate\") DESC",
"CreatedAt": "2024-01-16T08:00:00Z",
"CreatedBy": "admin",
"LastRefreshed": "2024-01-20T08:00:00Z",
"ColumnCount": 4,
"ReferencedBundles": ["Orders"],
"DataBundleName": "_mv_DailySales"
},
"ExecutionTimeMS": 4.12
}
Field Descriptions:
ViewName: Name of the viewDatabaseName: Database containing the viewType: "VIEW" or "MATERIALIZED_VIEW"Definition: Complete SELECT statementCreatedAt: ISO 8601 timestamp of creationCreatedBy: Username of creatorLastRefreshed: (Materialized views only) ISO 8601 timestamp of last refreshColumnCount: Number of columns in result setReferencedBundles: Array of bundles used in definitionDataBundleName: (Materialized views only) Name of storage bundleViews use a two-tier permission model:
Example:
-- View definition
CREATE VIEW "SalesReport" AS
SELECT * FROM "Orders" JOIN "Customers" ON "Orders"."CustomerID" == "Customers"."CustomerID";
-- Required permissions for user to query "SalesReport":
-- 1. SELECT on view "SalesReport"
-- 2. SELECT on bundle "Orders"
-- 3. SELECT on bundle "Customers"
Permissions are cached for 5 minutes to reduce overhead during query rewriting.
Cache Behavior:
Cache Invalidation:
Principle of Least Privilege
-- Grant view access without exposing full bundle
GRANT SELECT ON VIEW "PublicCustomerInfo" TO "web_app";
-- Don't grant: GRANT SELECT ON BUNDLE "Customers" TO "web_app";
Row-Level Security via Views
CREATE VIEW "MyOrders" AS
SELECT * FROM "Orders" WHERE "UserID" == CURRENT_USER();
Column-Level Security via Views
CREATE VIEW "CustomerBasicInfo" AS
SELECT "CustomerID", "Name", "Email" FROM "Customers";
-- Hides sensitive columns like SSN, CreditCard, etc.
Audit Trail via Views
CREATE VIEW "RecentActivity" AS
SELECT *, CURRENT_USER() AS "AccessedBy", F:NOW() AS "AccessedAt"
FROM "AuditLog";
✅ Good Use Cases:
❌ Poor Use Cases:
✅ Good Use Cases:
❌ Poor Use Cases:
Scenario: Daily sales aggregation on 10M orders
| Approach | First Query | Subsequent Queries | Storage | Freshness |
|---|---|---|---|---|
| Direct Query | 2.3s | 2.3s | 0 KB | Real-time |
| Regular View | 2.3s | 2.3s | 1 KB | Real-time |
| Materialized View | 2.3s (initial) | 5ms | 50 KB | Stale (manual refresh) |
Key Takeaways:
SyndrDB caches query plans for views to reduce planning overhead.
Cache Invalidation:
Benefits:
Monitoring:
EXPLAIN SELECT * FROM "CustomerStats";
Check for "CachedPlan" indicator in output.
-- Good: Descriptive, clear purpose
CREATE VIEW "ActiveCustomersUSA" AS ...;
CREATE MATERIALIZED VIEW "MonthlySalesByRegion" AS ...;
-- Bad: Vague, unclear
CREATE VIEW "v1" AS ...;
CREATE MATERIALIZED VIEW "temp" AS ...;
-- Good: Formatted for readability
CREATE VIEW "OrderSummary" AS
SELECT o."OrderID",
o."OrderDate",
c."CustomerName",
SUM(i."Amount") AS "TotalAmount"
FROM "Orders" AS o
JOIN "Customers" AS c ON o."CustomerID" == c."CustomerID"
JOIN "OrderItems" AS i ON o."OrderID" == i."OrderID"
GROUP BY o."OrderID", o."OrderDate", c."CustomerName";
-- Bad: Single line, hard to read
CREATE VIEW "OrderSummary" AS SELECT o."OrderID", o."OrderDate", c."CustomerName", SUM(i."Amount") AS "TotalAmount" FROM "Orders" AS o JOIN "Customers" AS c ON o."CustomerID" == c."CustomerID" JOIN "OrderItems" AS i ON o."OrderID" == i."OrderID" GROUP BY o."OrderID", o."OrderDate", c."CustomerName";
-- Good: Schedule during low-traffic periods
-- Via cron or application scheduler:
02:00 - REFRESH MATERIALIZED VIEW "DailySales";
02:30 - REFRESH MATERIALIZED VIEW "CustomerLTV";
03:00 - REFRESH MATERIALIZED VIEW "TopProducts";
-- Bad: Refreshing during peak hours
12:00 - REFRESH MATERIALIZED VIEW "DailySales"; -- Lunch rush!
-- Good: Document view purpose and refresh schedule
-- View: DailySales
-- Purpose: Aggregate daily order metrics for reporting dashboard
-- Refresh: Daily at 02:00 UTC via automated job
-- Owner: analytics_team
CREATE MATERIALIZED VIEW "DailySales" AS ...;
-- Good: Track view dependencies
-- DailySales depends on:
-- - Orders bundle
-- - If Orders schema changes, view must be recreated
CREATE MATERIALIZED VIEW "DailySales" AS
SELECT DATE("OrderDate") AS "Date",
COUNT(*) AS "OrderCount"
FROM "Orders"
GROUP BY DATE("OrderDate");
-- Good: Check if view exists before dropping
DESCRIBE VIEW "OldView";
-- If exists, then:
DROP VIEW "OldView";
-- Good: Validate view creation
CREATE VIEW "TestView" AS SELECT * FROM "TestBundle";
DESCRIBE VIEW "TestView"; -- Verify creation
SELECT * FROM "TestView" LIMIT 1; -- Test query
Setup:
-- Create materialized views for dashboard
CREATE MATERIALIZED VIEW "DailySalesSummary" AS
SELECT DATE("OrderDate") AS "Date",
COUNT(*) AS "TotalOrders",
SUM("Total") AS "Revenue",
AVG("Total") AS "AvgOrderValue",
COUNT(DISTINCT "CustomerID") AS "UniqueCustomers"
FROM "Orders"
WHERE "Status" == "Completed"
GROUP BY DATE("OrderDate")
ORDER BY DATE("OrderDate") DESC;
CREATE MATERIALIZED VIEW "TopSellingProducts" AS
SELECT p."ProductID",
p."ProductName",
p."Category",
COUNT(*) AS "OrderCount",
SUM(oi."Quantity") AS "QuantitySold",
SUM(oi."Amount") AS "Revenue"
FROM "OrderItems" AS oi
JOIN "Products" AS p ON oi."ProductID" == p."ProductID"
GROUP BY p."ProductID", p."ProductName", p."Category"
ORDER BY SUM(oi."Amount") DESC
LIMIT 50;
CREATE MATERIALIZED VIEW "CustomerSegments" AS
SELECT c."CustomerID",
c."CustomerName",
CASE
WHEN COUNT(o."OrderID") >= 10 THEN "VIP"
WHEN COUNT(o."OrderID") >= 5 THEN "Regular"
ELSE "Occasional"
END AS "Segment",
COUNT(o."OrderID") AS "OrderCount",
SUM(o."Total") AS "LifetimeValue"
FROM "Customers" AS c
LEFT JOIN "Orders" AS o ON c."CustomerID" == o."CustomerID"
GROUP BY c."CustomerID", c."CustomerName";
Daily Refresh (Scheduled at 02:00):
REFRESH MATERIALIZED VIEW "DailySalesSummary";
REFRESH MATERIALIZED VIEW "TopSellingProducts";
REFRESH MATERIALIZED VIEW "CustomerSegments";
Dashboard Query (Fast - reads from snapshots):
-- Revenue trend
SELECT * FROM "DailySalesSummary"
WHERE "Date" >= F:DATE_SUB(F:NOW(), 30, DAY)
ORDER BY "Date" DESC;
-- Top products
SELECT * FROM "TopSellingProducts" LIMIT 10;
-- Customer distribution
SELECT "Segment", COUNT(*) AS "Count", SUM("LifetimeValue") AS "TotalValue"
FROM "CustomerSegments"
GROUP BY "Segment";
Setup:
-- Hide sensitive customer data
CREATE VIEW "PublicCustomerInfo" AS
SELECT "CustomerID",
"CustomerName",
"Email",
"Country"
FROM "Customers";
-- Excludes: SSN, CreditCardHash, BillingAddress, etc.
-- Row-level security: Users see only their orders
CREATE VIEW "MyOrders" AS
SELECT *
FROM "Orders"
WHERE "CustomerID" == CURRENT_USER_ID();
-- Department-specific view
CREATE VIEW "SalesTeamView" AS
SELECT o."OrderID",
o."OrderDate",
c."CustomerName",
o."Total",
o."Status"
FROM "Orders" AS o
JOIN "Customers" AS c ON o."CustomerID" == c."CustomerID"
WHERE o."AssignedSalesRep" == CURRENT_USER();
Grant Permissions:
-- Web app only sees public info
GRANT SELECT ON VIEW "PublicCustomerInfo" TO "web_app_user";
-- Mobile app sees user's own orders
GRANT SELECT ON VIEW "MyOrders" TO "mobile_app_user";
-- Sales team sees their assigned orders
GRANT SELECT ON VIEW "SalesTeamView" TO "sales_team";
Setup:
CREATE MATERIALIZED VIEW "ComprehensiveSalesReport" AS
SELECT r."Region",
r."RegionName",
F:DATE_TRUNC(MONTH, o."OrderDate") AS "Month",
p."Category",
COUNT(DISTINCT o."OrderID") AS "OrderCount",
COUNT(DISTINCT o."CustomerID") AS "UniqueCustomers",
SUM(oi."Quantity") AS "TotalQuantity",
SUM(oi."Amount") AS "Revenue",
AVG(o."Total") AS "AvgOrderValue",
SUM(oi."Amount") - SUM(oi."Cost") AS "Profit",
(SUM(oi."Amount") - SUM(oi."Cost")) / SUM(oi."Amount") AS "ProfitMargin"
FROM "Orders" AS o
JOIN "OrderItems" AS oi ON o."OrderID" == oi."OrderID"
JOIN "Products" AS p ON oi."ProductID" == p."ProductID"
JOIN "Customers" AS c ON o."CustomerID" == c."CustomerID"
JOIN "Regions" AS r ON c."RegionID" == r."RegionID"
WHERE o."Status" == "Completed"
GROUP BY r."Region", r."RegionName", F:DATE_TRUNC(MONTH, o."OrderDate"), p."Category"
ORDER BY F:DATE_TRUNC(MONTH, o."OrderDate") DESC, r."Region", p."Category";
Benefits:
Monthly Refresh:
-- Refresh at month end
REFRESH MATERIALIZED VIEW "ComprehensiveSalesReport";
Query Examples:
-- Regional performance this year
SELECT "Region", "RegionName", SUM("Revenue") AS "TotalRevenue"
FROM "ComprehensiveSalesReport"
WHERE "Month" >= "2024-01-01"
GROUP BY "Region", "RegionName"
ORDER BY SUM("Revenue") DESC;
-- Category trends
SELECT "Category", "Month", SUM("Revenue") AS "Revenue"
FROM "ComprehensiveSalesReport"
WHERE "Month" >= F:DATE_SUB(F:NOW(), 12, MONTH)
GROUP BY "Category", "Month"
ORDER BY "Month" DESC, "Revenue" DESC;
Views provide:
Key Differences:
| Feature | Regular View | Materialized View |
|---|---|---|
| Storage | Definition only (~1KB) | Definition + data (varies) |
| Performance | Same as underlying query | Fast (pre-computed) |
| Freshness | Always current | Stale until refreshed |
| Use Case | Abstraction, security | Performance, analytics |
| Refresh | Automatic (query rewrite) | Manual (REFRESH command) |
Best Practices:
For more information, see: