Views — CREATE VIEW, Updatable Views and Indexed Views

A view is a named SELECT query stored in the database — it looks like a table to the caller but executes the underlying query each time. Views encapsulate complex joins, provide a stable API surface over changing tables, and restrict column access. Indexed views (materialised views) pre-compute and store the result, making aggregate queries instant. EF Core can query views exactly like tables using modelBuilder.Entity<T>().ToView("viewName"), making them a clean integration point between complex SQL and the application layer.

Creating and Using Views

-- ── Basic view — published post summary ────────────────────────────────────
CREATE OR ALTER VIEW dbo.vw_PublishedPostSummary
WITH SCHEMABINDING    -- prevents dropping referenced tables/columns
AS
SELECT
    p.Id,
    p.Title,
    p.Slug,
    p.Excerpt,
    p.CoverImageUrl,
    p.ViewCount,
    p.CommentCount,
    p.IsFeatured,
    p.PublishedAt,
    p.UpdatedAt,
    u.DisplayName   AS AuthorName,
    u.AvatarUrl     AS AuthorAvatarUrl,
    c.Name          AS CategoryName,
    c.Slug          AS CategorySlug
FROM   dbo.Posts      p
JOIN   dbo.Users      u ON u.Id  = p.AuthorId
LEFT   JOIN dbo.Categories c ON c.Id  = p.CategoryId
WHERE  p.IsPublished = 1
  AND  p.Status      = 'published';
GO

-- ── Query the view — same syntax as a table ───────────────────────────────
SELECT Id, Title, AuthorName, ViewCount
FROM   dbo.vw_PublishedPostSummary
WHERE  CategorySlug = 'dotnet'
ORDER  BY PublishedAt DESC;

-- ── Author statistics view ────────────────────────────────────────────────
CREATE OR ALTER VIEW dbo.vw_AuthorStats
WITH SCHEMABINDING
AS
SELECT
    u.Id            AS AuthorId,
    u.DisplayName,
    u.AvatarUrl,
    COUNT_BIG(p.Id) AS TotalPosts,       -- COUNT_BIG required for indexed views
    SUM(p.ViewCount)    AS TotalViews,
    SUM(p.CommentCount) AS TotalComments,
    MAX(p.PublishedAt)  AS LastPublishedAt
FROM   dbo.Users u
JOIN   dbo.Posts p ON p.AuthorId = u.Id
WHERE  p.IsPublished = 1
GROUP  BY u.Id, u.DisplayName, u.AvatarUrl;
GO

-- ── Indexed view — materialise the aggregate ──────────────────────────────
-- Requirements: SCHEMABINDING, deterministic expressions, specific options
CREATE UNIQUE CLUSTERED INDEX IX_vw_AuthorStats
ON dbo.vw_AuthorStats (AuthorId);
-- SQL Server now stores the pre-computed result and updates it on DML changes
-- Querying the view uses the index rather than re-running the aggregation

-- ── WITH CHECK OPTION — prevent inserts/updates that violate the view filter ─
CREATE OR ALTER VIEW dbo.vw_DraftPosts
WITH SCHEMABINDING
AS
SELECT Id, Title, Status, AuthorId
FROM   dbo.Posts
WHERE  Status = 'draft'
WITH   CHECK OPTION;  -- prevents UPDATE that would change Status away from 'draft'
GO

-- ── View metadata ─────────────────────────────────────────────────────────
SELECT name, create_date, modify_date, is_schema_bound
FROM   sys.views
WHERE  name LIKE 'vw_%';

-- ── Drop view safely ──────────────────────────────────────────────────────
DROP VIEW IF EXISTS dbo.vw_OldView;
Note: WITH SCHEMABINDING on a view prevents the underlying tables and columns from being dropped or altered while the view exists. It also enables the view to be used as the target for an indexed view (materialised view). Always add WITH SCHEMABINDING to production views. Note that schema-bound views must use two-part names (dbo.Posts not just Posts) and cannot use SELECT * — both good practices regardless of schema binding.
Tip: Indexed views (materialised views) dramatically accelerate aggregate queries but add overhead to every INSERT, UPDATE, and DELETE on the underlying tables — SQL Server must maintain the materialised result. Use indexed views for stable aggregates that are queried frequently and updated infrequently: author statistics, category post counts, tag popularity. Do not create indexed views on tables with high write throughput unless you have benchmarked the read/write trade-off.
Warning: Views do not automatically improve performance — a non-indexed view is just a macro that is expanded into the calling query at parse time. Querying vw_PublishedPostSummary WHERE CategorySlug = 'dotnet' executes the same query as writing the full SQL inline. The benefit of views is encapsulation and reuse, not speed. For performance gains, use indexed views (materialised) or ensure the underlying tables have appropriate indexes that the view’s query can use.

EF Core Integration

// ── Map a keyless entity to a view ────────────────────────────────────────
public class PublishedPostSummary  // no Id property needed for keyless
{
    public int     Id            { get; init; }
    public string  Title         { get; init; } = "";
    public string  Slug          { get; init; } = "";
    public string? Excerpt       { get; init; }
    public int     ViewCount     { get; init; }
    public string  AuthorName    { get; init; } = "";
    public string? CategorySlug  { get; init; }
    public DateTime? PublishedAt { get; init; }
}

// In DbContext:
protected override void OnModelCreating(ModelBuilder mb)
{
    mb.Entity<PublishedPostSummary>()
      .HasNoKey()
      .ToView("vw_PublishedPostSummary");
}

// Query the view:
var posts = await _db.Set<PublishedPostSummary>()
    .Where(p => p.CategorySlug == "dotnet")
    .OrderByDescending(p => p.PublishedAt)
    .ToListAsync(ct);

Common Mistakes

Mistake 1 — SELECT * in a schema-bound view (syntax error)

❌ Wrong — CREATE VIEW vw_Posts WITH SCHEMABINDING AS SELECT * FROM dbo.Posts; SELECT * not allowed with SCHEMABINDING.

✅ Correct — always list columns explicitly: SELECT p.Id, p.Title, ....

Mistake 2 — Assuming views automatically improve query performance

❌ Wrong — creating a view expecting it to be faster; without an index it is just syntactic sugar over the same query.

✅ Correct — non-indexed views are for encapsulation; add a clustered index (indexed view) for materialised performance gains.

🧠 Test Yourself

A view vw_PublishedPostSummary has WITH SCHEMABINDING. A developer tries to DROP TABLE dbo.Users. What happens?