A database upgrade can look simple on paper: install the new version, migrate the schema, run the application, and verify that everything works.
Performance upgrades are rarely that simple.
A new PostgreSQL major version can change query-planner behavior, which means the same SQL generated by an EF Core application may produce a different execution plan after an upgrade. Sometimes that change improves performance. Sometimes it has little measurable effect. In certain workloads, a different plan can even expose a regression that was not visible on the previous version.
PostgreSQL 19 is currently in beta, with Beta 2 released on July 16, 2026. The PostgreSQL project specifically encourages developers to test their typical workloads against the beta before the final release, while also advising against using beta releases in production. The project currently plans PostgreSQL 19 for September 2026.
That makes this a good time to build a controlled benchmark with a real .NET application rather than relying on feature lists or synthetic database tests.
This article shows how to compare PostgreSQL planner behavior for an EF Core workload and identify meaningful differences in execution plans, latency, reads, and resource consumption.
Why Planner Changes Matter to EF Core Applications
EF Core does not execute LINQ directly against PostgreSQL.
The basic flow looks like this:
LINQ Query
|
v
EF Core Query Translation
|
v
Generated SQL
|
v
PostgreSQL Parser
|
v
Query Planner / Optimizer
|
v
Execution Plan
|
v
Query Result
The application may generate exactly the same SQL against two PostgreSQL major versions.
The planner can still choose different execution strategies.
For example, a query might use:
Index Scan
on one version and:
Bitmap Heap Scan
-> Bitmap Index Scan
on another.
The difference is important because query planning depends on statistics, estimated row counts, available indexes, table size, configuration, and planner behavior.
PostgreSQL's documentation describes the planner as the component responsible for considering possible execution strategies and selecting a plan based on estimated costs.
That means a database upgrade should be benchmarked using application-shaped queries.
What PostgreSQL 19 Beta Means for This Benchmark
There is an important distinction between testing PostgreSQL 19 Beta and benchmarking a final production release.
PostgreSQL explicitly states that beta and release-candidate versions are intended for testing and are not recommended for production systems. Behavior and implementation details can still change before the final release.
Therefore, this benchmark should answer questions such as:
Does the planner choose different plans?
Are those plans better for our workload?
Which queries change?
Does execution time improve or regress?
Are estimated and actual row counts significantly different?
Do index usage patterns change?
Does EF Core application latency change?
It should not be presented as a final PostgreSQL 19 performance verdict.
Build a Representative EF Core Workload
Start with an application model that resembles a typical business API.
For example:
public class Order
{
public long Id { get; set; }
public long CustomerId { get; set; }
public DateTime CreatedAt { get; set; }
public string Status { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
}
The corresponding DbContext can be configured normally:
public class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public AppDbContext(
DbContextOptions<AppDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasIndex(x => new
{
x.CustomerId,
x.CreatedAt
});
modelBuilder.Entity<Order>()
.HasIndex(x => new
{
x.Status,
x.CreatedAt
});
}
}
The important point is that the benchmark should use realistic indexes.
Removing indexes simply to make the planner comparison more dramatic produces results that may not represent a real application.
Create a Representative Query
Consider an API that retrieves recent completed orders for a customer:
var orders = await db.Orders
.AsNoTracking()
.Where(x =>
x.CustomerId == customerId &&
x.Status == "Completed" &&
x.CreatedAt >= startDate)
.OrderByDescending(x => x.CreatedAt)
.Take(100)
.ToListAsync();
This query contains several planner-relevant characteristics:
Equality filtering
Multiple predicates
Date filtering
Ordering
Limiting results
Composite indexes
Potentially large tables
These characteristics make it more useful for an upgrade benchmark than a trivial SELECT * query.
Capture the SQL Generated by EF Core
Before comparing PostgreSQL versions, capture the SQL generated by the same EF Core version.
You can inspect generated SQL using:
var query = db.Orders
.AsNoTracking()
.Where(x =>
x.CustomerId == customerId &&
x.Status == "Completed" &&
x.CreatedAt >= startDate)
.OrderByDescending(x => x.CreatedAt)
.Take(100);
Console.WriteLine(query.ToQueryString());
The important rule is consistency.
Use the same:
EF Core version
Provider version
Application code
Query
Parameters
Schema
Indexes
Dataset
The PostgreSQL server version should be the primary variable being changed.
Otherwise, it becomes difficult to determine what caused a performance difference.
Use EXPLAIN ANALYZE
Once you have the generated SQL, run it through PostgreSQL's execution-plan tools.
A useful starting point is:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
"Id",
"CustomerId",
"CreatedAt",
"Status",
"TotalAmount"
FROM "Orders"
WHERE "CustomerId" = 1001
AND "Status" = 'Completed'
AND "CreatedAt" >= TIMESTAMP '2026-01-01'
ORDER BY "CreatedAt" DESC
LIMIT 100;
EXPLAIN shows the planner's selected execution strategy.
ANALYZE actually executes the query and provides runtime information.
BUFFERS adds information about buffer activity, which can help explain why two plans with similar execution times may have different I/O behavior.
Do not run EXPLAIN ANALYZE blindly against destructive statements. For benchmark work, begin with read-only queries.
What to Compare Between Versions
The most useful comparison is not simply:
PostgreSQL 18 = 14 ms
PostgreSQL 19 = 11 ms
That number alone does not explain why the result changed.
Compare the plans.
| Metric | PostgreSQL Version A | PostgreSQL Version B | Why It Matters |
|---|
| Planning Time | Measure | Measure | Indicates planning overhead |
| Execution Time | Measure | Measure | Primary runtime metric |
| Rows Returned | Measure | Measure | Confirms equivalent results |
| Shared Buffers Hit | Measure | Measure | Indicates cached page activity |
| Shared Buffers Read | Measure | Measure | Indicates physical reads |
| Scan Type | Record | Record | Reveals plan changes |
| Estimated Rows | Record | Record | Shows planner estimates |
| Actual Rows | Record | Record | Shows estimation accuracy |
| Join Strategy | Record | Record | Important for relational workloads |
| Sort Strategy | Record | Record | Useful for ordered queries |
The exact metrics will vary by query.
Benchmark the Same Dataset
A database planner makes decisions based partly on statistics.
That makes dataset consistency critical.
For example:
PostgreSQL 18
|
+-- 10 million Orders
+-- Same indexes
+-- Same statistics strategy
+-- Same configuration
PostgreSQL 19 Beta
|
+-- 10 million Orders
+-- Same indexes
+-- Same statistics strategy
+-- Same configuration
If one database has significantly different statistics, the comparison may measure data-preparation differences rather than planner changes.
After loading the benchmark data, ensure statistics are current.
For example:
ANALYZE "Orders";
The PostgreSQL documentation notes that planner statistics are an important input into row estimation and cost calculation.
Benchmark Cold and Warm Cache Behavior
Database cache state can dramatically affect results.
A warm-cache test asks:
What happens when the required database pages are already cached?
A cold or less-warm test asks a different question:
What happens when data must be read from storage?
Both can be useful.
Do not mix them together and call the resulting average a single benchmark number.
Instead, label your tests clearly:
Warm-cache benchmark
Cold-cache benchmark
Also run multiple iterations instead of relying on one execution.
Benchmark More Than One Query
A planner change that helps one query does not automatically improve the application.
Create a small workload suite.
For example:
Query 1: Customer order lookup
Query 2: Recent orders by status
Query 3: Revenue aggregation
Query 4: Pagination query
Query 5: Multi-table customer/order query
Query 6: Dashboard summary
The workload should represent the queries that matter to the application.
A useful benchmark result might therefore look like:
| Query | Plan Changed? | Execution Change | Buffer Change | Result |
|---|
| Customer Orders | Yes | Measure | Measure | Improved/Regressed |
| Status Search | No | Measure | Measure | Stable |
| Revenue Summary | Yes | Measure | Measure | Improved/Regressed |
| Pagination | Yes | Measure | Measure | Improved/Regressed |
| Dashboard | No | Measure | Measure | Stable |
This is far more informative than publishing one aggregate benchmark score.
Measuring EF Core Application Performance
Database execution time is only one layer.
A real API request includes:
HTTP Request
|
v
ASP.NET Core
|
v
EF Core
|
v
Npgsql
|
v
PostgreSQL
|
v
Result Materialization
|
v
HTTP Response
Measure both database-level and application-level performance.
For example:
var stopwatch = Stopwatch.StartNew();
var orders = await db.Orders
.AsNoTracking()
.Where(x =>
x.CustomerId == customerId &&
x.Status == "Completed")
.OrderByDescending(x => x.CreatedAt)
.Take(100)
.ToListAsync();
stopwatch.Stop();
Console.WriteLine(
$"Rows: {orders.Count}, " +
$"Elapsed: {stopwatch.ElapsedMilliseconds} ms");
This gives you an application-side measurement.
The PostgreSQL execution plan gives you the database-side measurement.
Using both helps identify where a difference actually occurs.
Common Benchmarking Mistakes
Changing EF Core and PostgreSQL at the Same Time
If you upgrade both the database and ORM/provider, you cannot confidently attribute the result to PostgreSQL's planner.
Keep the variables controlled.
Using Different Data Volumes
A plan that works well for 100,000 rows may not be optimal for 100 million rows.
Use production-shaped data volumes where possible.
Ignoring Statistics
Different statistics can result in different row estimates and plans.
Run the same preparation process for both databases.
Comparing Only One Query
One query is not an application benchmark.
Use a representative workload.
Using Beta Results as Production Guarantees
PostgreSQL 19 Beta 2 is explicitly a testing release. The PostgreSQL project encourages workload testing but does not recommend beta versions for production.
Publishing Unsupported Conclusions
If a query becomes 15% faster in one test, that does not mean PostgreSQL 19 is universally 15% faster.
Report the workload, environment, dataset, query, configuration, and methodology.
Troubleshooting Unexpected Plan Changes
If PostgreSQL chooses a different plan, first check whether the inputs actually match.
Verify:
Same SQL.
Same parameter values.
Same schema.
Same indexes.
Same data distribution.
Current statistics.
Comparable PostgreSQL configuration.
Comparable cache conditions.
Same hardware or equivalent resources.
Same benchmark methodology.
If estimated rows differ significantly from actual rows, investigate statistics before concluding that the planner itself is responsible.
For example, this query can expose the execution plan and runtime behavior:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT ...
Then compare the plan node by node.
A Practical Benchmark Workflow
A repeatable PostgreSQL/EF Core upgrade experiment can follow these steps:
Select five to ten production-representative queries.
Capture the SQL generated by the current EF Core version.
Create identical database schemas.
Load equivalent datasets.
Apply the same indexes.
Refresh statistics.
Run baseline tests against the existing PostgreSQL version.
Run the same tests against PostgreSQL 19 Beta.
Capture EXPLAIN (ANALYZE, BUFFERS) output.
Compare execution plans and application latency.
Investigate queries with meaningful plan changes.
Repeat suspicious results before drawing conclusions.
This process turns a database upgrade from a compatibility exercise into a measurable performance experiment.
Frequently Asked Questions
Does PostgreSQL 19 automatically make EF Core queries faster?
No. PostgreSQL planner improvements can benefit particular workloads, but performance depends on the query, data distribution, indexes, statistics, configuration, and execution environment.
Should PostgreSQL 19 Beta be used in production?
No. PostgreSQL explicitly advises against using beta releases in production systems. Beta releases are intended for testing and feedback.
Do I need to change my EF Core LINQ queries?
Not necessarily. The purpose of this benchmark is to determine how the same application workload behaves on different PostgreSQL versions. Query changes should be treated as a separate optimization experiment.
Is execution time enough to compare planner changes?
No. Execution time is important, but execution plans, row estimates, buffer activity, and application-level latency provide additional context.
Should every query be benchmarked?
You do not need to benchmark every query in a large application. Start with high-volume, latency-sensitive, and business-critical queries. Expand the workload if the initial results reveal meaningful changes.
Best Practices
For a reliable PostgreSQL planner benchmark with EF Core:
Keep the EF Core and provider versions constant.
Keep schema and indexes identical.
Use the same dataset.
Refresh statistics consistently.
Test representative queries.
Capture execution plans.
Measure application-level latency.
Separate warm-cache and cold-cache tests.
Run multiple iterations.
Record configuration and hardware details.
Investigate plan changes instead of looking only at elapsed time.
Treat beta results as pre-release findings, not production guarantees.
Conclusion
PostgreSQL major-version upgrades deserve more than a compatibility test. For applications built with EF Core, the database planner is an important part of the performance equation, and a change in execution strategy can affect real API behavior even when the application's LINQ code remains unchanged.
PostgreSQL 19 Beta provides an opportunity to test those differences before the major release. The PostgreSQL project itself encourages developers to run representative workloads against the beta and use the results to identify bugs and regressions.
The most useful benchmark is therefore not a headline such as "PostgreSQL 19 is faster." It is a reproducible comparison showing which EF Core queries changed plans, how those plans affected execution and I/O, and whether the differences matter to the application.
That approach gives development and database teams concrete evidence they can use when deciding whether an upgrade is ready for their workload.