dotnet / dotnet/efcore

Use of partial/filtered indexes for FK columns on TPH tables

Open
#37,742 1 comment 0 reactions 0 assignees View on GitHub
area-model-building area-perf customer-reported
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

### Question

On TPH tables, there are separate columns for each derived type with indexes looking something like this (using PostgreSQL):

```sql
CREATE UNIQUE INDEX "IX_BaseType_DerivedTypeId" ON public."BaseTypes" ("DerivedTypeId")
```
I'm wondering what the reasons are for these not being made partial/filtered by default:

```sql
CREATE UNIQUE INDEX "IX_BaseType_DerivedTypeId" ON public."BaseTypes" ("DerivedTypeId")
WHERE "DerivedTypeId" IS NOT NULL
```

(see posted code for a slightly more complete example and actual generated SQL from EF 9.0.2)

Having partial (ie filtered) indices substantially reduces their size on large/wide tables with no apparent (?) downside. As far as I can tell, the only thing that will be impacted is if one were to do something like
```csharp
var results = context.Entity.Where(x => x.ForeignEntityId == null).ToList();
```
which on large tables - even with a non-partial index - tend to perform poorly due to low selectivity and might easily cause the planner to opt for a full table scan anyway.

We have several wide TPH tables in our application with >100M rows, and it feels a bit cumbersome to manually override each and every index EF generates here to avoid the severe bloat that comes with having them non-partial. We're currently using ModelBuilder to handle this automatically per entity, but the code seems fairly complex/brittle, so we're on the lookout for a better/more idiomatic approach.

I've tried looking around the various ef/npgsql repos for any discussion on this, but have come up short. Would greatly appreciate any advice/pointers here.

(Originally posted on https://github.com/npgsql/efcore.pg/issues/3735)

### Your code

```csharp
public class RootEntity
{
public int Id { get; set; }
public SubEntityA A { get; set; } = null!;
public SubEntityB B { get; set; } = null!;
public SubEntityC C { get; set; } = null!;
}

public abstract class BaseEntity
{
public int Id { get; set; }
}

public class SubEntityA : BaseEntity;
public class SubEntityB : BaseEntity;
public class SubEntityC : BaseEntity;

// ...

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().HasOne(x => x.A).WithOne().HasForeignKey("RootEntityId").IsRequired();
modelBuilder.Entity().HasOne(x => x.B).WithOne().HasForeignKey("RootEntityId").IsRequired();
modelBuilder.Entity().HasOne(x => x.C).WithOne().HasForeignKey("RootEntityId").IsRequired();

}

/*
This yields the following SQL in PostgreSQL:
*/

CREATE TABLE "BaseEntities" (
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
"Discriminator" character varying(13) NOT NULL,
"RootEntityId" integer,
"SubEntityB_RootEntityId" integer,
"SubEntityC_RootEntityId" integer,
CONSTRAINT "PK_BaseEntities" PRIMARY KEY ("Id"),
CONSTRAINT "FK_BaseEntities_RootEntities_RootEntityId" FOREIGN KEY ("RootEntityId") REFERENCES "RootEntities" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_BaseEntities_RootEntities_SubEntityB_RootEntityId" FOREIGN KEY ("SubEntityB_RootEntityId") REFERENCES "RootEntities" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_BaseEntities_RootEntities_SubEntityC_RootEntityId" FOREIGN KEY ("SubEntityC_RootEntityId") REFERENCES "RootEntities" ("Id") ON DELETE CASCADE
);

-- Note there's no filter defined; each index contains the full amount
-- of rows in the table (so even if 99.9% of the table consists is SubEntityB rows,
-- all indices are equally large in terms of bytes, wasting RAM/shared_buffers)
CREATE UNIQUE INDEX "IX_BaseEntities_RootEntityId" ON "BaseEntities" ("RootEntityId");
CREATE UNIQUE INDEX "IX_BaseEntities_SubEntityB_RootEntityId" ON "BaseEntities" ("SubEntityB_RootEntityId");
CREATE UNIQUE INDEX "IX_BaseEntities_SubEntityC_RootEntityId" ON "BaseEntities" ("SubEntityC_RootEntityId");
```

### Stack traces

```text

```

### Verbose output

```text

```

### EF Core version

9.0.2

### Database provider

Npgsql.EntityFrameworkCore.PostgreSQL

### Target framework

.NET10

### Operating system

MacOS Tahoe 26.3

### IDE

Rider 2025.3.1

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.