@@ -16,6 +16,7 @@ internal static class PostgreSqlStorageMath
1616 private const double IndexPageHeaderBytes = 24 ;
1717 private const double BTreeSpecialSpaceBytes = 16 ;
1818 private const double IndexTupleHeaderBytes = 8 ;
19+ private const double IndexNullBitmapHeaderBytes = 16 ;
1920 private const double IndexRowPointerBytes = 4 ;
2021 private const int MaxAlignment = 8 ;
2122 private const int DefaultVariableLengthBytes = 256 ;
@@ -107,11 +108,15 @@ private static EntityStorageEstimate EstimateEntity(
107108 . Select ( property => EstimateProperty ( property , $ "{ entityPath } .{ property . Name } ", warnings ) )
108109 . ToArray ( ) ;
109110 var propertyLookup = propertyEstimates . ToDictionary ( property => property . Name , StringComparer . Ordinal ) ;
110- var tupleHeaderBytes = Align (
111+ var tupleHeaderBytesWithoutNulls = Align ( HeapTupleHeaderBytes , MaxAlignment ) ;
112+ var tupleHeaderBytesWithNulls = Align (
111113 HeapTupleHeaderBytes + GetNullBitmapBytes ( properties ) ,
112114 MaxAlignment ) ;
113- var tupleDataBytes = propertyEstimates . Sum ( property => property . AverageStoredBytes ) ;
114- var averageHeapRowBytes = Align ( tupleHeaderBytes + tupleDataBytes , MaxAlignment ) + HeapItemPointerBytes ;
115+ var averageHeapTupleBytes = EstimateAverageTupleBytes (
116+ tupleHeaderBytesWithoutNulls ,
117+ tupleHeaderBytesWithNulls ,
118+ propertyEstimates ) ;
119+ var averageHeapRowBytes = averageHeapTupleBytes + HeapItemPointerBytes ;
115120 var heapBytes = EstimatePagedBytes ( rowCount , averageHeapRowBytes , HeapPageHeaderBytes , 0 ) ;
116121 var indexEstimates = indexes
117122 . Select ( index => EstimateIndex ( index , propertyLookup , rowCount ) )
@@ -138,7 +143,11 @@ private static PropertySizeEstimate EstimateProperty(
138143 warnings . Add (
139144 $ "Using fallback fill rate 1.0 for nullable fixed-width property '{ propertyPath } '. Add [{ nameof ( StorageFieldAttribute ) } ] to override it.") ;
140145
141- return new PropertySizeEstimate ( property . Name , size * fillRate ) ;
146+ return new PropertySizeEstimate (
147+ property . Name ,
148+ size ,
149+ GetFixedAlignment ( property , propertyPath , warnings ) ,
150+ fillRate ) ;
142151 }
143152
144153
@@ -184,13 +193,23 @@ private static PropertySizeEstimate EstimateVariableLengthProperty(
184193 }
185194
186195 if ( IsNumericType ( property . ClrType , property . ProviderClrType , property . StoreType ) )
187- return new PropertySizeEstimate ( property . Name , averageLength . Value * fillRate ) ;
196+ return new PropertySizeEstimate (
197+ property . Name ,
198+ averageLength . Value ,
199+ GetVariableLengthAlignment ( averageLength . Value ) ,
200+ fillRate ) ;
188201
189202 var overheadBytes = averageLength <= 126
190203 ? 1
191204 : 4 ;
192205
193- return new PropertySizeEstimate ( property . Name , ( averageLength . Value + overheadBytes ) * fillRate ) ;
206+ var storedBytes = averageLength . Value + overheadBytes ;
207+
208+ return new PropertySizeEstimate (
209+ property . Name ,
210+ storedBytes ,
211+ GetVariableLengthAlignment ( storedBytes ) ,
212+ fillRate ) ;
194213 }
195214
196215
@@ -213,8 +232,14 @@ private static StorageIndexEstimate EstimateIndex(
213232 IReadOnlyDictionary < string , PropertySizeEstimate > propertyLookup ,
214233 double rowCount )
215234 {
216- var keyBytes = index . Properties . Sum ( property => propertyLookup [ property . Name ] . AverageStoredBytes ) ;
217- var averageEntryBytes = Align ( IndexTupleHeaderBytes + keyBytes , MaxAlignment ) + IndexRowPointerBytes ;
235+ var keyProperties = index . Properties
236+ . Select ( property => propertyLookup [ property . Name ] )
237+ . ToArray ( ) ;
238+ var averageTupleBytes = EstimateAverageTupleBytes (
239+ IndexTupleHeaderBytes ,
240+ IndexNullBitmapHeaderBytes ,
241+ keyProperties ) ;
242+ var averageEntryBytes = averageTupleBytes + IndexRowPointerBytes ;
218243 var estimatedBytes = EstimatePagedBytes ( rowCount , averageEntryBytes , IndexPageHeaderBytes , BTreeSpecialSpaceBytes ) + HeapPageBytes ;
219244
220245 return new StorageIndexEstimate (
@@ -226,6 +251,143 @@ private static StorageIndexEstimate EstimateIndex(
226251 }
227252
228253
254+ private static double EstimateAverageTupleBytes (
255+ double headerBytesWithoutNulls ,
256+ double headerBytesWithNulls ,
257+ IReadOnlyList < PropertySizeEstimate > properties )
258+ {
259+ if ( properties . Count == 0 )
260+ return Align ( headerBytesWithoutNulls , MaxAlignment ) ;
261+
262+ var allPresentProbability = properties . Aggregate ( 1.0d , ( current , property ) => current * property . FillRate ) ;
263+ var allPresentBytesWithoutNulls = EstimateTupleBytesForAlwaysPresentProperties ( headerBytesWithoutNulls , properties ) ;
264+
265+ if ( allPresentProbability >= 1.0d )
266+ return allPresentBytesWithoutNulls ;
267+
268+ var allPresentBytesWithNulls = EstimateTupleBytesForAlwaysPresentProperties ( headerBytesWithNulls , properties ) ;
269+ var expectedBytesWithNullableHeader = EstimateExpectedTupleBytes (
270+ headerBytesWithNulls ,
271+ properties ) ;
272+
273+ return ( allPresentProbability * allPresentBytesWithoutNulls ) +
274+ expectedBytesWithNullableHeader -
275+ ( allPresentProbability * allPresentBytesWithNulls ) ;
276+ }
277+
278+
279+ private static double EstimateTupleBytesForAlwaysPresentProperties (
280+ double headerBytes ,
281+ IReadOnlyList < PropertySizeEstimate > properties )
282+ {
283+ var offset = headerBytes ;
284+
285+ foreach ( var property in properties )
286+ {
287+ offset = Align ( offset , property . AlignmentBytes ) ;
288+ offset += property . BytesWhenPresent ;
289+ }
290+
291+ return Align ( offset , MaxAlignment ) ;
292+ }
293+
294+
295+ private static double EstimateExpectedTupleBytes (
296+ double headerBytes ,
297+ IReadOnlyList < PropertySizeEstimate > properties )
298+ {
299+ var states = new Dictionary < int , TupleLayoutState >
300+ {
301+ [ GetResidue ( headerBytes ) ] = new TupleLayoutState ( 1.0d , headerBytes )
302+ } ;
303+
304+ foreach ( var property in properties )
305+ {
306+ var nextStates = new Dictionary < int , TupleLayoutState > ( ) ;
307+
308+ foreach ( var state in states )
309+ {
310+ var residue = state . Key ;
311+ var layoutState = state . Value ;
312+
313+ if ( property . FillRate < 1.0d )
314+ AddState (
315+ nextStates ,
316+ residue ,
317+ new TupleLayoutState (
318+ layoutState . Probability * ( 1.0d - property . FillRate ) ,
319+ layoutState . WeightedOffsetSum * ( 1.0d - property . FillRate ) ) ) ;
320+
321+ if ( property . FillRate <= 0.0d )
322+ continue ;
323+
324+ var paddingBytes = GetPaddingBytes ( residue , property . AlignmentBytes ) ;
325+ var addedBytes = paddingBytes + property . BytesWhenPresent ;
326+
327+ AddProbability (
328+ nextStates ,
329+ GetResidue ( residue + addedBytes ) ,
330+ new TupleLayoutState (
331+ layoutState . Probability * property . FillRate ,
332+ ( layoutState . WeightedOffsetSum * property . FillRate ) + ( layoutState . Probability * property . FillRate * addedBytes ) ) ) ;
333+ }
334+
335+ states = nextStates ;
336+ }
337+
338+ return states . Sum ( state => state . Value . WeightedOffsetSum + ( state . Value . Probability * GetPaddingBytes ( state . Key , MaxAlignment ) ) ) ;
339+ }
340+
341+
342+ private static int GetPaddingBytes (
343+ int residue ,
344+ int alignmentBytes )
345+ {
346+ if ( alignmentBytes <= 1 )
347+ return 0 ;
348+
349+ var misalignment = residue % alignmentBytes ;
350+
351+ return misalignment == 0
352+ ? 0
353+ : alignmentBytes - misalignment ;
354+ }
355+
356+
357+ private static int GetResidue ( double offset )
358+ {
359+ return ( ( int ) Math . Round ( offset , MidpointRounding . AwayFromZero ) ) % MaxAlignment ;
360+ }
361+
362+
363+ private static void AddProbability (
364+ IDictionary < int , TupleLayoutState > states ,
365+ int residue ,
366+ TupleLayoutState state )
367+ {
368+ if ( state . Probability <= 0.0d )
369+ return ;
370+
371+ AddState ( states , residue , state ) ;
372+ }
373+
374+
375+ private static void AddState (
376+ IDictionary < int , TupleLayoutState > states ,
377+ int residue ,
378+ TupleLayoutState state )
379+ {
380+ if ( state . Probability <= 0.0d )
381+ return ;
382+
383+ states [ residue ] = states . TryGetValue ( residue , out var current )
384+ ? new TupleLayoutState (
385+ current . Probability + state . Probability ,
386+ current . WeightedOffsetSum + state . WeightedOffsetSum )
387+ : state ;
388+ }
389+
390+
229391 private static double EstimatePagedBytes (
230392 double rowCount ,
231393 double averageEntryBytes ,
@@ -251,7 +413,7 @@ private static double GetNullBitmapBytes(IReadOnlyList<PropertySizingInput> prop
251413 }
252414
253415
254- private static double GetFixedSize (
416+ private static int GetFixedSize (
255417 PropertySizingInput property ,
256418 string propertyPath ,
257419 ICollection < string > warnings )
@@ -323,6 +485,64 @@ private static double GetFixedSize(
323485 }
324486
325487
488+ private static int GetFixedAlignment (
489+ PropertySizingInput property ,
490+ string propertyPath ,
491+ ICollection < string > warnings )
492+ {
493+ var nonNullableClrType = Nullable . GetUnderlyingType ( property . ClrType ) ?? property . ClrType ;
494+ var nonNullableProviderType = Nullable . GetUnderlyingType ( property . ProviderClrType ) ?? property . ProviderClrType ;
495+ var storeType = property . StoreType ?? string . Empty ;
496+
497+ if ( nonNullableProviderType . IsEnum )
498+ nonNullableProviderType = Enum . GetUnderlyingType ( nonNullableProviderType ) ;
499+
500+ if ( nonNullableClrType . IsEnum )
501+ nonNullableClrType = Enum . GetUnderlyingType ( nonNullableClrType ) ;
502+
503+ if ( nonNullableProviderType == typeof ( bool ) || nonNullableClrType == typeof ( bool ) ||
504+ nonNullableProviderType == typeof ( byte ) || nonNullableProviderType == typeof ( sbyte ) ||
505+ nonNullableClrType == typeof ( byte ) || nonNullableClrType == typeof ( sbyte ) ||
506+ nonNullableProviderType == typeof ( Guid ) || nonNullableClrType == typeof ( Guid ) ||
507+ storeType . Contains ( "uuid" , StringComparison . OrdinalIgnoreCase ) )
508+ return 1 ;
509+
510+ if ( nonNullableProviderType == typeof ( short ) || nonNullableProviderType == typeof ( ushort ) ||
511+ nonNullableClrType == typeof ( short ) || nonNullableClrType == typeof ( ushort ) )
512+ return 2 ;
513+
514+ if ( nonNullableProviderType == typeof ( int ) || nonNullableProviderType == typeof ( uint ) ||
515+ nonNullableClrType == typeof ( int ) || nonNullableClrType == typeof ( uint ) ||
516+ nonNullableProviderType == typeof ( float ) || nonNullableClrType == typeof ( float ) ||
517+ storeType . Contains ( "date" , StringComparison . OrdinalIgnoreCase ) )
518+ return 4 ;
519+
520+ if ( nonNullableProviderType == typeof ( long ) || nonNullableProviderType == typeof ( ulong ) ||
521+ nonNullableClrType == typeof ( long ) || nonNullableClrType == typeof ( ulong ) ||
522+ nonNullableProviderType == typeof ( double ) || nonNullableClrType == typeof ( double ) ||
523+ nonNullableProviderType == typeof ( DateTime ) || nonNullableClrType == typeof ( DateTime ) ||
524+ nonNullableProviderType == typeof ( DateTimeOffset ) || nonNullableClrType == typeof ( DateTimeOffset ) ||
525+ nonNullableProviderType == typeof ( TimeSpan ) || nonNullableClrType == typeof ( TimeSpan ) ||
526+ storeType . Contains ( "timestamp" , StringComparison . OrdinalIgnoreCase ) ||
527+ storeType . Contains ( "interval" , StringComparison . OrdinalIgnoreCase ) ||
528+ storeType . Contains ( "time with time zone" , StringComparison . OrdinalIgnoreCase ) ||
529+ storeType . Contains ( "time" , StringComparison . OrdinalIgnoreCase ) )
530+ return 8 ;
531+
532+ warnings . Add ( $ "Using fallback fixed-size alignment 8 for '{ propertyPath } ' because its PostgreSQL alignment could not be inferred.") ;
533+
534+ return 8 ;
535+ }
536+
537+
538+ private static int GetVariableLengthAlignment ( double storedBytes )
539+ {
540+ return storedBytes <= 126
541+ ? 1
542+ : 4 ;
543+ }
544+
545+
326546 private static bool IsNumericType (
327547 Type clrType ,
328548 Type providerClrType ,
@@ -512,12 +732,25 @@ public StoragePropertySchema ToSchema()
512732
513733 private sealed class PropertySizeEstimate (
514734 string name ,
515- double averageStoredBytes )
735+ double bytesWhenPresent ,
736+ int alignmentBytes ,
737+ double fillRate )
516738 {
517739 public string Name { get ; } = name ;
518740
519- public double AverageStoredBytes { get ; } = averageStoredBytes ;
741+ public double BytesWhenPresent { get ; } = bytesWhenPresent ;
742+
743+ public int AlignmentBytes { get ; } = alignmentBytes ;
744+
745+ public double FillRate { get ; } = fillRate ;
746+
747+ public double AverageStoredBytes => BytesWhenPresent * FillRate ;
520748 }
521749
750+
751+ private readonly record struct TupleLayoutState (
752+ double Probability ,
753+ double WeightedOffsetSum ) ;
754+
522755 #endregion
523756}
0 commit comments