@@ -127,6 +127,28 @@ const getOrderClause = (
127127 return `${ fieldExpr } ${ direction } `
128128}
129129
130+ const detectPinnedMemberId = ( filterString : string ) : { pinned : boolean ; smallList : boolean } => {
131+ if ( ! filterString ) return { pinned : false , smallList : false }
132+
133+ // m.id = '...'
134+ const eqRe = / \b m \. i d \s * = \s * (?: ' [ ^ ' ] + ' | \$ \( [ ^ ) ] + \) | : [ a - z A - Z _ ] \w * | \? ) / i
135+ if ( eqRe . test ( filterString ) ) return { pinned : true , smallList : true }
136+
137+ // m.id IN ( ... ) → estimate list size by counting commas (rough but effective)
138+ const inRe = / \b m \. i d \s + I N \s * \( ( [ ^ ) ] + ) \) / i
139+ const m = inRe . exec ( filterString )
140+ if ( m && m [ 1 ] ) {
141+ const items = m [ 1 ]
142+ . split ( ',' )
143+ . map ( ( s ) => s . trim ( ) )
144+ . filter ( Boolean )
145+ // Consider "small" lists up to ~100 items; tune if needed.
146+ return { pinned : true , smallList : items . length <= 100 }
147+ }
148+
149+ return { pinned : false , smallList : false }
150+ }
151+
130152export const buildQuery = ( {
131153 fields,
132154 withAggregates,
@@ -141,38 +163,68 @@ export const buildQuery = ({
141163 const fallbackDir : OrderDirection = orderDirection || 'DESC'
142164 const { field : sortField , direction } = parseOrderBy ( orderBy , fallbackDir )
143165
144- // If filter references mo.*, we must ensure member_orgs is joined in the outer query.
166+ // Detect alias usage in filters (to decide joins/CTEs needed outside)
145167 const filterHasMo = filterString . includes ( 'mo.' )
168+ const filterHasMe = filterString . includes ( 'me.' )
146169 const needsMemberOrgs = includeMemberOrgs || filterHasMo
147170
148- // We use the optimized path when:
149- // - aggregates are requested (msa available)
150- // - sorting is by activityCount (or not specified → default)
151- // NOTE: we DO NOT check for mo./me. here because we keep all filters OUTSIDE the CTE.
152- const useActivityCountOptimized = withAggregates && ( ! sortField || sortField === 'activityCount' )
171+ // If filters pin m.id to a single value or a small IN-list, skip top-N entirely.
172+ const { pinned, smallList } = detectPinnedMemberId ( filterString )
173+ const useDirectIdPath = withAggregates && pinned && smallList
174+
175+ // Default sort clause for fallback/outer queries
176+ const orderClause = getOrderClause ( sortField , direction , withAggregates )
177+
178+ if ( useDirectIdPath ) {
179+ // Direct path: start from memberSegmentsAgg keyed by (memberId, segmentId)
180+ const ctes : string [ ] = [ ]
181+ if ( needsMemberOrgs ) ctes . push ( buildMemberOrgsCTE ( true ) . trim ( ) )
182+
183+ const withClause = ctes . length ? `WITH ${ ctes . join ( ',\n' ) } ` : ''
153184
154- log . info ( `buildQuery: useActivityCountOptimized=${ useActivityCountOptimized } ` )
185+ const memberOrgsJoin = needsMemberOrgs ? `LEFT JOIN member_orgs mo ON mo."memberId" = m.id` : ''
186+
187+ // NOTE:
188+ // - We do NOT include member_search here; an ID-pin makes it redundant.
189+ // - We keep the full filterString (it already contains the m.id predicate).
190+ // - This path leverages the UNIQUE (memberId, segmentId) index for O(1) lookups.
191+ return `
192+ ${ withClause }
193+ SELECT ${ fields }
194+ FROM "memberSegmentsAgg" msa
195+ JOIN members m
196+ ON m.id = msa."memberId"
197+ ${ memberOrgsJoin }
198+ LEFT JOIN "memberEnrichments" me
199+ ON me."memberId" = m.id
200+ WHERE
201+ msa."segmentId" = $(segmentId)
202+ AND (${ filterString } )
203+ ORDER BY ${ orderClause } NULLS LAST
204+ LIMIT ${ limit }
205+ OFFSET ${ offset }
206+ ` . trim ( )
207+ }
208+
209+ // Decide if we can use the activityCount-optimized path
210+ const useActivityCountOptimized =
211+ withAggregates && ( ! sortField || sortField === 'activityCount' ) && ! filterHasMe
212+ // (we do allow mo.* now, but only outside the CTE; see below)
155213
156214 if ( useActivityCountOptimized ) {
157215 const ctes : string [ ] = [ ]
158216
159- // Include member_orgs CTE only if we need it in the OUTER query (never inside top_members)
160- if ( needsMemberOrgs ) {
161- ctes . push ( buildMemberOrgsCTE ( true ) . trim ( ) )
162- }
217+ // Include member_orgs CTE only for the OUTER query (never filter inside the CTE)
218+ if ( needsMemberOrgs ) ctes . push ( buildMemberOrgsCTE ( true ) . trim ( ) )
163219
164- // Include search CTE if present
165- if ( searchConfig . cte ) {
166- ctes . push ( searchConfig . cte . trim ( ) )
167- }
220+ // Include search CTE if present, but join it to msa inside top_members via memberId
221+ if ( searchConfig . cte ) ctes . push ( searchConfig . cte . trim ( ) )
168222
169- // Join search to msa WITHOUT touching members (so index on msa can be used)
170223 const searchJoinForTopMembers = searchConfig . cte
171224 ? `\n INNER JOIN member_search ms ON ms."memberId" = msa."memberId"`
172225 : ''
173226
174- // Oversample: fetch more rows than needed before applying outer filters,
175- // then apply LIMIT/OFFSET on the final ordered result.
227+ // Oversample to keep page filled after outer filters; tune multiplier if needed
176228 const oversampleMultiplier = 5
177229 const prefetch = Math . max ( limit * oversampleMultiplier + offset , limit + offset )
178230
@@ -194,13 +246,9 @@ export const buildQuery = ({
194246 )
195247
196248 const withClause = `WITH ${ ctes . join ( ',\n' ) } `
197-
198249 const memberOrgsJoin = needsMemberOrgs ? `LEFT JOIN member_orgs mo ON mo."memberId" = m.id` : ''
199250
200- // IMPORTANT:
201- // - All filters on members/orgs/enrichments are applied OUTSIDE the CTE.
202- // - Final ORDER BY keeps activityCount (already aligned with top_members).
203- // - Final LIMIT/OFFSET ensure correct pagination after applying filters.
251+ // Outer filters (including mo./me.) applied here; index handles the CTE ranking
204252 return `
205253 ${ withClause }
206254 SELECT ${ fields }
@@ -221,7 +269,7 @@ export const buildQuery = ({
221269 ` . trim ( )
222270 }
223271
224- // Fallback path: any other sort mode.
272+ // Fallback path ( other sorts / non-aggregate)
225273 const baseCtes = [ needsMemberOrgs ? buildMemberOrgsCTE ( true ) : '' , searchConfig . cte ] . filter (
226274 Boolean ,
227275 )
@@ -235,10 +283,8 @@ export const buildQuery = ({
235283 searchConfig . join ,
236284 ] . filter ( Boolean )
237285
238- const orderClause = getOrderClause ( sortField , direction , withAggregates )
239-
240286 return `
241- ${ baseCtes . length > 0 ? `WITH ${ baseCtes . join ( ',\n' ) } ` : '' }
287+ ${ baseCtes . length ? `WITH ${ baseCtes . join ( ',\n' ) } ` : '' }
242288 SELECT ${ fields }
243289 FROM members m
244290 ${ joins . join ( '\n' ) }
@@ -248,6 +294,7 @@ export const buildQuery = ({
248294 OFFSET ${ offset }
249295 ` . trim ( )
250296}
297+
251298export const buildCountQuery = ( {
252299 withAggregates,
253300 searchConfig,
0 commit comments