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",
DATEDIFF(NOW(), 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", 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" >= DATEADD(DAY, -30, NOW())
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",
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", DATE_TRUNC('month', o."OrderDate"), p."Category"
ORDER BY 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" >= DATEADD(MONTH, -12, NOW())
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: