Every write to a flag in a v2-versioned environment opens a new EnvironmentFeatureVersion and publishes it. Both halves of that cost grow with the number of feature states the flag has, so a flag with many segment overrides pays a query count proportional to its overrides.
Measured on main with CaptureQueriesContext around EnvironmentFeatureVersion.objects.create(...) and version.publish(...), for a flag with 1 and with 3 segment overrides (2 and 4 feature states):
| Step |
1 override |
3 overrides |
Per extra state |
EnvironmentFeatureVersion.objects.create |
26 |
54 |
~8 |
version.publish |
45 |
61 |
~5 |
Extrapolated, a flag with 50 segment overrides costs roughly 400 queries to open a version and 250 to publish it. Every v2 write path pays this: the versioning serializers, change request commits, and the experimental update-flag endpoints alike.
Creating a version copies row by row
FeatureState.clone (api/features/models.py:661) is a deepcopy plus save() per state, and FeatureSegment.clone (api/features/models.py:328) does the same. Per cloned state the create issues, roughly:
SELECT features_feature
SELECT features_featurestatevalue
INSERT features_featurestate and INSERT features_historicalfeaturestate
INSERT features_featurestatevalue and INSERT features_historicalfeaturestatevalue
- a
SAVEPOINT / RELEASE pair
Some copying is inherent to immutable versions, but the row-at-a-time shape is not: bulk_create for the states and their values, with the history rows batched alongside, would collapse most of it.
Publishing re-fetches relations per state
This half looks like a plain N+1 rather than inherent work. Between 1 and 3 overrides, publish goes from 45 to 61 queries, and the growth is all in per-state relation lookups:
| Query |
1 override |
3 overrides |
SELECT multivariate_multivariatefeaturestatevalue |
6 |
10 |
SELECT features_feature |
4 |
8 |
SELECT features_featurestatevalue |
4 |
8 |
SELECT features_featuresegment |
3 |
7 |
Whatever the publish path and its signal handlers iterate should be able to hydrate these with select_related / prefetch_related in one pass.
Suggested scope
- Flatten the publish-side N+1 first — it is the cheaper fix and is not intrinsic to versioning.
- Then consider bulk-creating the cloned states, values and multivariate values when a version is created.
Worth pinning either fix with a query-count test at the service level.
Every write to a flag in a v2-versioned environment opens a new
EnvironmentFeatureVersionand publishes it. Both halves of that cost grow with the number of feature states the flag has, so a flag with many segment overrides pays a query count proportional to its overrides.Measured on
mainwithCaptureQueriesContextaroundEnvironmentFeatureVersion.objects.create(...)andversion.publish(...), for a flag with 1 and with 3 segment overrides (2 and 4 feature states):EnvironmentFeatureVersion.objects.createversion.publishExtrapolated, a flag with 50 segment overrides costs roughly 400 queries to open a version and 250 to publish it. Every v2 write path pays this: the versioning serializers, change request commits, and the experimental update-flag endpoints alike.
Creating a version copies row by row
FeatureState.clone(api/features/models.py:661) is adeepcopyplussave()per state, andFeatureSegment.clone(api/features/models.py:328) does the same. Per cloned state the create issues, roughly:SELECT features_featureSELECT features_featurestatevalueINSERT features_featurestateandINSERT features_historicalfeaturestateINSERT features_featurestatevalueandINSERT features_historicalfeaturestatevalueSAVEPOINT/RELEASEpairSome copying is inherent to immutable versions, but the row-at-a-time shape is not:
bulk_createfor the states and their values, with the history rows batched alongside, would collapse most of it.Publishing re-fetches relations per state
This half looks like a plain N+1 rather than inherent work. Between 1 and 3 overrides,
publishgoes from 45 to 61 queries, and the growth is all in per-state relation lookups:SELECT multivariate_multivariatefeaturestatevalueSELECT features_featureSELECT features_featurestatevalueSELECT features_featuresegmentWhatever the publish path and its signal handlers iterate should be able to hydrate these with
select_related/prefetch_relatedin one pass.Suggested scope
Worth pinning either fix with a query-count test at the service level.