From a74c0027d44e4293bd44313ea7d1bf1d144b502e Mon Sep 17 00:00:00 2001 From: gbutler Date: Thu, 18 Jun 2026 10:51:32 -0500 Subject: [PATCH 1/2] refactor(speakers): replace nested IN(SELECT DISTINCT) with correlated EXISTS in getUniqueActivitiesCountBySummit The previous implementation wrapped the filter subquery output in IN(SELECT DISTINCT speaker_ids), forcing MySQL to materialise the full intermediate set and probe it per outer presentation row. Restructured as a single correlated EXISTS rooted at PresentationSpeaker: the inner QB checks assignment or moderator membership against the outer presentation p directly, with filter predicates appended inline. MySQL can now use the Presentation_Speakers(PresentationID) index and short-circuit on the first match per row. --- .../Summit/DoctrineSpeakerRepository.php | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/app/Repositories/Summit/DoctrineSpeakerRepository.php b/app/Repositories/Summit/DoctrineSpeakerRepository.php index 237042028..2a5dd6c41 100644 --- a/app/Repositories/Summit/DoctrineSpeakerRepository.php +++ b/app/Repositories/Summit/DoctrineSpeakerRepository.php @@ -705,23 +705,22 @@ function ($query) { */ public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter = null): int { - // Inner query: distinct IDs of speakers who belong to this summit (via assignment - // or moderator role) and match any caller-supplied filter. Uses IDENTITY() with a - // scalar summit ID so no entity parameter is embedded in the getDQL() string — - // entity parameters copied into an outer QB via getDQL() are not correctly resolved - // to their primary key by Doctrine's type system. + // Build the EXISTS inner QB rooted at PresentationSpeaker (e) with the joins that + // filter mappings expect (m, rr). The initial WHERE ties e to the outer presentation + // p via assignment or moderator role; filter conditions are then appended by + // apply2Query. The EXISTS approach lets MySQL use the + // index on Presentation_Speakers(PresentationID) to reach speakers for each p directly + // and short-circuit on the first match. $innerQb = $this->getEntityManager()->createQueryBuilder() - ->select('e.id') - ->distinct(true) + ->select('1') ->from('models\summit\PresentationSpeaker', 'e') - ->leftJoin('e.registration_request', 'rr') ->leftJoin('e.member', 'm') + ->leftJoin('e.registration_request', 'rr') ->where( 'EXISTS (SELECT 1 FROM App\Models\Foundation\Summit\Speakers\PresentationSpeakerAssignment __a' - . ' JOIN __a.presentation __ap WHERE IDENTITY(__ap.summit) = :summit_id AND __a.speaker = e)' - . ' OR EXISTS (SELECT 1 FROM models\summit\Presentation __mp WHERE IDENTITY(__mp.summit) = :summit_id AND __mp.moderator = e)' - ) - ->setParameter('summit_id', $summit->getId()); + . ' WHERE __a.presentation = p AND __a.speaker = e)' + . ' OR p.moderator = e' + ); if (!is_null($filter)) { $filter->apply2Query($innerQb, $this->getFilterMappings($filter)); @@ -729,30 +728,22 @@ public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter $innerDql = $innerQb->getDQL(); - // Outer query counts distinct presentations where at least one matched speaker is - // either an assigned speaker or the moderator. The inner DQL is embedded exactly - // once (inside a single wrapper EXISTS) to avoid Doctrine alias-conflict errors. - $outerQb = $this->getEntityManager()->createQueryBuilder() + $qb = $this->getEntityManager()->createQueryBuilder() ->select('COUNT(DISTINCT p.id)') ->from('models\summit\Presentation', 'p') ->where('p.summit = :summit') - ->andWhere( - 'EXISTS (' - . 'SELECT 1 FROM models\summit\PresentationSpeaker __spk' - . ' WHERE __spk.id IN (' . $innerDql . ')' - . ' AND (' - . 'EXISTS (SELECT 1 FROM App\Models\Foundation\Summit\Speakers\PresentationSpeakerAssignment __cnt WHERE __cnt.presentation = p AND __cnt.speaker = __spk)' - . ' OR p.moderator = __spk' - . ')' - . ')' - ) + ->andWhere('EXISTS (' . $innerDql . ')') ->setParameter('summit', $summit); + // Copy filter-specific parameter bindings (e.g. :value_N for text/id filters). + // Filter sub-queries reference :summit by name; that is satisfied by the binding above. foreach ($innerQb->getParameters() as $param) { - $outerQb->setParameter($param->getName(), $param->getValue()); + if ($param->getName() !== 'summit') { + $qb->setParameter($param->getName(), $param->getValue(), $param->getType()); + } } - return intval($outerQb->getQuery()->getSingleScalarResult()); + return intval($qb->getQuery()->getSingleScalarResult()); } /** From f85637c3cc153f4e563d9846d84b486a46a23403 Mon Sep 17 00:00:00 2001 From: gbutler Date: Mon, 22 Jun 2026 10:01:55 -0500 Subject: [PATCH 2/2] refactor(speakers): rewrite getUniqueActivitiesCountBySummit to eliminate correlated subquery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the correlated EXISTS (which scanned all PresentationSpeaker rows per outer presentation) with two non-correlated IN subqueries — one for the assignment path, one for the moderator path — each embedding the filtered speaker set once. Presentation ID lists are merged and deduplicated in PHP. MySQL can now materialise the speaker set once rather than re-executing per presentation row. --- .../Summit/DoctrineSpeakerRepository.php | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/app/Repositories/Summit/DoctrineSpeakerRepository.php b/app/Repositories/Summit/DoctrineSpeakerRepository.php index 2a5dd6c41..2fdf546a0 100644 --- a/app/Repositories/Summit/DoctrineSpeakerRepository.php +++ b/app/Repositories/Summit/DoctrineSpeakerRepository.php @@ -705,22 +705,13 @@ function ($query) { */ public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter = null): int { - // Build the EXISTS inner QB rooted at PresentationSpeaker (e) with the joins that - // filter mappings expect (m, rr). The initial WHERE ties e to the outer presentation - // p via assignment or moderator role; filter conditions are then appended by - // apply2Query. The EXISTS approach lets MySQL use the - // index on Presentation_Speakers(PresentationID) to reach speakers for each p directly - // and short-circuit on the first match. + // Build a non-correlated inner QB that selects IDs of speakers matching the filter. + // MySQL can materialise this set once per query rather than re-executing per row. $innerQb = $this->getEntityManager()->createQueryBuilder() - ->select('1') + ->select('e.id') ->from('models\summit\PresentationSpeaker', 'e') ->leftJoin('e.member', 'm') - ->leftJoin('e.registration_request', 'rr') - ->where( - 'EXISTS (SELECT 1 FROM App\Models\Foundation\Summit\Speakers\PresentationSpeakerAssignment __a' - . ' WHERE __a.presentation = p AND __a.speaker = e)' - . ' OR p.moderator = e' - ); + ->leftJoin('e.registration_request', 'rr'); if (!is_null($filter)) { $filter->apply2Query($innerQb, $this->getFilterMappings($filter)); @@ -728,22 +719,35 @@ public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter $innerDql = $innerQb->getDQL(); - $qb = $this->getEntityManager()->createQueryBuilder() - ->select('COUNT(DISTINCT p.id)') + // Q1: presentation IDs where a qualifying speaker is formally assigned. + $assignmentQb = $this->getEntityManager()->createQueryBuilder() + ->select('p.id') ->from('models\summit\Presentation', 'p') + ->join('p.speakers', 'a') ->where('p.summit = :summit') - ->andWhere('EXISTS (' . $innerDql . ')') + ->andWhere('a.speaker IN (' . $innerDql . ')') ->setParameter('summit', $summit); - // Copy filter-specific parameter bindings (e.g. :value_N for text/id filters). - // Filter sub-queries reference :summit by name; that is satisfied by the binding above. foreach ($innerQb->getParameters() as $param) { - if ($param->getName() !== 'summit') { - $qb->setParameter($param->getName(), $param->getValue(), $param->getType()); - } + $assignmentQb->setParameter($param->getName(), $param->getValue(), $param->getType()); + } + + // Q2: presentation IDs where the moderator is a qualifying speaker. + $moderatorQb = $this->getEntityManager()->createQueryBuilder() + ->select('p.id') + ->from('models\summit\Presentation', 'p') + ->where('p.summit = :summit') + ->andWhere('p.moderator IN (' . $innerDql . ')') + ->setParameter('summit', $summit); + + foreach ($innerQb->getParameters() as $param) { + $moderatorQb->setParameter($param->getName(), $param->getValue(), $param->getType()); } - return intval($qb->getQuery()->getSingleScalarResult()); + $assignmentIds = array_column($assignmentQb->getQuery()->getScalarResult(), 'id'); + $moderatorIds = array_column($moderatorQb->getQuery()->getScalarResult(), 'id'); + + return count(array_unique(array_merge($assignmentIds, $moderatorIds))); } /**