Skip to content

Commit 314bba6

Browse files
committed
Option to use PG row count estimation
1 parent b1008fe commit 314bba6

11 files changed

Lines changed: 77 additions & 17 deletions

File tree

src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ namespace MultiDbContextExample.Repositories;
1010
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
1111
public sealed class DbContextARepository<TResource>(
1212
ITargetedFields targetedFields, DbContextResolver<DbContextA> dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory,
13-
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor)
13+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor,
14+
IJsonApiOptions options)
1415
: EntityFrameworkCoreRepository<TResource, long>(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory,
15-
resourceDefinitionAccessor)
16+
resourceDefinitionAccessor, options)
1617
where TResource : class, IIdentifiable<long>;

src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ namespace MultiDbContextExample.Repositories;
1010
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
1111
public sealed class DbContextBRepository<TResource>(
1212
ITargetedFields targetedFields, DbContextResolver<DbContextB> dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory,
13-
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor)
13+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor,
14+
IJsonApiOptions options)
1415
: EntityFrameworkCoreRepository<TResource, long>(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory,
15-
resourceDefinitionAccessor)
16+
resourceDefinitionAccessor, options)
1617
where TResource : class, IIdentifiable<long>;

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ public interface IJsonApiOptions
104104
/// </summary>
105105
bool IncludeTotalResourceCount { get; }
106106

107+
/// <summary>
108+
/// Whether to use the PostgreSQL row estimate from <c>pg_class.reltuples</c> instead of an exact <c>COUNT(*)</c> query when
109+
/// <see cref="IncludeTotalResourceCount" /> is <c>true</c>. Only applies when using PostgreSQL and no filter is active; falls back to exact count
110+
/// otherwise. The estimate is updated by VACUUM/ANALYZE and may be inaccurate for tables that have not yet been analyzed. <c>false</c> by default.
111+
/// </summary>
112+
bool UseEstimatedResourceCount { get; }
113+
107114
/// <summary>
108115
/// The page size (10 by default) that is used when not specified in query string. Set to <c>null</c> to not use pagination by default. This setting can
109116
/// be overruled per relationship by setting <see cref="HasManyAttribute.DisablePagination" /> to <c>true</c>.

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public sealed class JsonApiOptions : IJsonApiOptions
5858
/// <inheritdoc />
5959
public bool IncludeTotalResourceCount { get; set; }
6060

61+
/// <inheritdoc />
62+
public bool UseEstimatedResourceCount { get; set; }
63+
6164
/// <inheritdoc />
6265
public PageSize? DefaultPageSize { get; set; } = new(10);
6366

src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ public class EntityFrameworkCoreRepository<TResource, TId> : IResourceRepository
3737
private readonly IResourceFactory _resourceFactory;
3838
private readonly IQueryConstraintProvider[] _constraintProviders;
3939
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
40+
private readonly IJsonApiOptions _options;
4041
private readonly TraceLogWriter<EntityFrameworkCoreRepository<TResource, TId>> _traceWriter;
4142

4243
/// <inheritdoc />
4344
public virtual string? TransactionId => _dbContext.Database.CurrentTransaction?.TransactionId.ToString();
4445

4546
public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph,
4647
IResourceFactory resourceFactory, IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory,
47-
IResourceDefinitionAccessor resourceDefinitionAccessor)
48+
IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options)
4849
{
4950
ArgumentNullException.ThrowIfNull(targetedFields);
5051
ArgumentNullException.ThrowIfNull(dbContextResolver);
@@ -53,13 +54,15 @@ public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextR
5354
ArgumentNullException.ThrowIfNull(constraintProviders);
5455
ArgumentNullException.ThrowIfNull(loggerFactory);
5556
ArgumentNullException.ThrowIfNull(resourceDefinitionAccessor);
57+
ArgumentNullException.ThrowIfNull(options);
5658

5759
_targetedFields = targetedFields;
5860
_dbContext = dbContextResolver.GetContext();
5961
_resourceGraph = resourceGraph;
6062
_resourceFactory = resourceFactory;
6163
_constraintProviders = constraintProviders as IQueryConstraintProvider[] ?? constraintProviders.ToArray();
6264
_resourceDefinitionAccessor = resourceDefinitionAccessor;
65+
_options = options;
6366
_traceWriter = new TraceLogWriter<EntityFrameworkCoreRepository<TResource, TId>>(loggerFactory);
6467
}
6568

@@ -95,6 +98,16 @@ public virtual async Task<int> CountAsync(FilterExpression? filter, Cancellation
9598

9699
using (CodeTimingSessionManager.Current.Measure("Repository - Count resources"))
97100
{
101+
if (_options.UseEstimatedResourceCount && filter == null)
102+
{
103+
int? estimate = await TryGetPostgresEstimatedCountAsync(cancellationToken);
104+
105+
if (estimate != null)
106+
{
107+
return estimate.Value;
108+
}
109+
}
110+
98111
ResourceType resourceType = _resourceGraph.GetResourceType<TResource>();
99112

100113
var layer = new QueryLayer(resourceType)
@@ -111,6 +124,37 @@ public virtual async Task<int> CountAsync(FilterExpression? filter, Cancellation
111124
}
112125
}
113126

127+
private async Task<int?> TryGetPostgresEstimatedCountAsync(CancellationToken cancellationToken)
128+
{
129+
string? providerName = _dbContext.Database.ProviderName;
130+
131+
if (providerName == null || (!providerName.Contains("PostgreSQL", StringComparison.OrdinalIgnoreCase) &&
132+
!providerName.Contains("Npgsql", StringComparison.OrdinalIgnoreCase)))
133+
{
134+
return null;
135+
}
136+
137+
IEntityType? entityType = _dbContext.Model.FindEntityType(typeof(TResource));
138+
string? tableName = entityType?.GetTableName();
139+
140+
if (tableName == null)
141+
{
142+
return null;
143+
}
144+
145+
string? schema = entityType!.GetSchema();
146+
147+
using (CodeTimingSessionManager.Current.Measure("Execute SQL (estimated count)", MeasurementSettings.ExcludeDatabaseInPercentages))
148+
{
149+
long estimate = await _dbContext.Database.SqlQuery<long>(
150+
$"SELECT reltuples::bigint FROM pg_class INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace WHERE pg_class.relname = {tableName} AND pg_namespace.nspname = COALESCE({schema}, current_schema())")
151+
.FirstOrDefaultAsync(cancellationToken);
152+
153+
// reltuples = -1 means statistics have not been collected yet; fall back to exact count.
154+
return estimate < 0 ? null : (int)Math.Min(estimate, int.MaxValue);
155+
}
156+
}
157+
114158
protected virtual IQueryable<TResource> ApplyQueryLayer(QueryLayer queryLayer)
115159
{
116160
ArgumentNullException.ThrowIfNull(queryLayer);

test/DiscoveryTests/PrivateResourceRepository.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace DiscoveryTests;
1010
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
1111
public sealed class PrivateResourceRepository(
1212
ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory,
13-
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor)
13+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor,
14+
IJsonApiOptions options)
1415
: EntityFrameworkCoreRepository<PrivateResource, long>(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders,
15-
loggerFactory, resourceDefinitionAccessor);
16+
loggerFactory, resourceDefinitionAccessor, options);

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public sealed class LyricRepository : EntityFrameworkCoreRepository<Lyric, long>
1616

1717
public LyricRepository(ExtraDbContext extraDbContext, ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph,
1818
IResourceFactory resourceFactory, IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory,
19-
IResourceDefinitionAccessor resourceDefinitionAccessor)
20-
: base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor)
19+
IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options)
20+
: base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor, options)
2121
{
2222
_extraDbContext = extraDbContext;
2323

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Transactions;
1010
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
1111
public sealed class MusicTrackRepository(
1212
ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory,
13-
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor)
13+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor,
14+
IJsonApiOptions options)
1415
: EntityFrameworkCoreRepository<MusicTrack, Guid>(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory,
15-
resourceDefinitionAccessor)
16+
resourceDefinitionAccessor, options)
1617
{
1718
public override string? TransactionId => null;
1819
}

test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarCompositeKeyAwareRepository.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.CompositeKeys;
1111
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
1212
public class CarCompositeKeyAwareRepository<TResource, TId>(
1313
ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory,
14-
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor)
14+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor,
15+
IJsonApiOptions options)
1516
: EntityFrameworkCoreRepository<TResource, TId>(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory,
16-
resourceDefinitionAccessor)
17+
resourceDefinitionAccessor, options)
1718
where TResource : class, IIdentifiable<TId>
1819
{
1920
private readonly CarExpressionRewriter _writer = new(resourceGraph);

test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/BuildingRepository.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.EagerLoading;
1010
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
1111
public sealed class BuildingRepository(
1212
ITargetedFields targetedFields, IDbContextResolver dbContextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory,
13-
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor)
13+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor,
14+
IJsonApiOptions options)
1415
: EntityFrameworkCoreRepository<Building, long>(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory,
15-
resourceDefinitionAccessor)
16+
resourceDefinitionAccessor, options)
1617
{
1718
public override async Task<Building> GetForCreateAsync(Type resourceClrType, long id, CancellationToken cancellationToken)
1819
{

0 commit comments

Comments
 (0)