`add_shape` is O(node population) for unindexed clauses: the whole `other_shapes` map is copied in and out of ETS per insert · Issue #4743 · electric-sql/electric · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
add_shape is O(node population) for unindexed clauses: the whole other_shapes map is copied in and out of ETS per insert #4743
Each insert costs O(shapes already on that node), so building a set of m unindexed shapes on one node costs O(m²). remove_shape has the same cost.
Any shape whose where clause is not fully optimized lands here, so ordinary workloads reach this path.
Expected: adding a shape costs about the same regardless of how many shapes are already on the
node.
Actual: per-shape cost grows linearly with the node's population.
Versions: Electric sync-service v1.7.8, also reproduced on main @ 917589195. Plain HTTP client; this is server-side filter behaviour and does not depend on the client.
Impact
Filter.add_shape/3 on v1.7.8, µs per shape, where n is the number of shapes accumulating on the
same node. K is the size of an IN list in the clause, which sets the residual size.
K
n
add µs/shape
10
100
191
10
1,000
2,539
10
5,000
14,616
45
5,000
61,341
150
1,000
36,151
Per-shape cost grows roughly linearly with n, so total build cost is quadratic. We could not build the n = 20,000 configurations within a 700s per-segment budget; those are reported as not built rather than extrapolated.
This also affects startup, since restoring shapes re-adds every shape to the filter and pays the same map-copy cost on each insert.
Suggested direction
Store other_shapes entries as individual ETS rows keyed by {condition_id, shape_id, branch_key} rather than as one map inside a single row. Insert and delete become O(1), and the linear scan at routing time becomes a bounded-prefix match/select over the same rows. This is the approach #4134 already used for the subquery index with :ordered_set.
We can open a PR if that direction is useful.
Related
#4742 reports the clause pattern that put large numbers of shapes on a single node in our case, an indexable IN/OR conjunct being dropped from the index when ANDed with a non-optimized condition. The two are independent, since this one applies to any other_shapes population however it arises, but they compound: #4742 causes shapes that should have been indexed to accumulate here
instead.
Environment
Electric sync-service v1.7.8, also verified on main @ 917589195.
Elixir 1.20.2 / OTP 29.0.2.
Written with AI assistance and reviewed by a human before filing. The timings are measured rather than estimated, on one machine. We can share the benchmark harness if the numbers are hard to reproduce.
I’d like to work on this issue. I’ll investigate the current ETS representation of other_shapes and the related insert/remove and lookup paths, then evaluate the proposed approach of storing entries as individual ETS rows. I’ll make sure the change preserves the existing routing behaviour and add regression/benchmark coverage to verify that insert and delete performance no longer grows with the number of shapes on a node.
Activity
AbarnaaSree commentedon Aug 23, 2026
I’d like to work on this issue. I’ll investigate the current ETS representation of
other_shapesand the related insert/remove and lookup paths, then evaluate the proposed approach of storing entries as individual ETS rows. I’ll make sure the change preserves the existing routing behaviour and add regression/benchmark coverage to verify that insert and delete performance no longer grows with the number of shapes on a node.Store filter other_shapes as one ETS row per shape