Improved performance when loading visits chuncks at high offsets

This commit is contained in:
Alejandro Celaya 2020-05-03 18:18:24 +02:00
parent 0e4bccc4bb
commit 8b0ce8e6f3

View File

@ -5,10 +5,16 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use function preg_replace;
use const PHP_INT_MAX;
class VisitRepository extends EntityRepository implements VisitRepositoryInterface
{
@ -82,19 +88,60 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa
?int $limit = null,
?int $offset = null
): array {
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange);
$qb->select('v', 'vl')
->leftJoin('v.visitLocation', 'vl')
/**
* @var QueryBuilder $qb
* @var ShortUrl|int $shortUrl
*/
[$qb, $shortUrl] = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange);
$qb->select('v.id')
->orderBy('v.id', 'DESC')
->setMaxResults($limit)
->setFirstResult($offset);
// Falling back to values that will behave as no limit/offset, but will workaround MS SQL not allowing
// order on sub-queries without offset
->setMaxResults($limit ?? PHP_INT_MAX)
->setFirstResult($offset ?? 0);
return $qb->getQuery()->getResult();
// FIXME Crappy way to resolve the params into the query. Best option would be to inject the sub-query with
// placeholders and then pass params to the main query
$shortUrlId = $shortUrl instanceof ShortUrl ? $shortUrl->getId() : $shortUrl;
$subQuery = $qb->getQuery()->getSQL();
$subQuery = preg_replace('/\?/', $shortUrlId, $subQuery, 1);
if ($dateRange !== null && $dateRange->getStartDate() !== null) {
$subQuery = preg_replace(
'/\?/',
'\'' . $dateRange->getStartDate()->toDateTimeString() . '\'',
$subQuery,
1,
);
}
if ($dateRange !== null && $dateRange->getEndDate() !== null) {
$subQuery = preg_replace('/\?/', '\'' . $dateRange->getEndDate()->toDateTimeString() . '\'', $subQuery, 1);
}
// A native query builder needs to be used here because DQL and ORM query builders do not accept
// sub-queries at "from" and "join" level.
// If no sub-query is used, then the performance drops dramatically while the "offset" grows.
$nativeQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
$nativeQb->select('v.*', 'vl.*')
->from('visits', 'v')
->join('v', '(' . $subQuery . ')', 'o', $nativeQb->expr()->eq('o.id_0', 'v.id'))
->leftJoin('v', 'visit_locations', 'vl', $nativeQb->expr()->eq('v.visit_location_id', 'vl.id'))
->orderBy('v.id', 'DESC');
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
$rsm->addRootEntityFromClassMetadata(Visit::class, 'v');
$rsm->addJoinedEntityFromClassMetadata(VisitLocation::class, 'vl', 'v', 'visitLocation', [
'id' => 'visit_location_id',
]);
$query = $this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm);
return $query->getResult();
}
public function countVisitsByShortCode(string $shortCode, ?string $domain = null, ?DateRange $dateRange = null): int
{
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange);
/** @var QueryBuilder $qb */
[$qb] = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange);
$qb->select('COUNT(v.id)');
return (int) $qb->getQuery()->getSingleScalarResult();
@ -104,7 +151,7 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa
string $shortCode,
?string $domain,
?DateRange $dateRange
): QueryBuilder {
): array {
/** @var ShortUrlRepositoryInterface $shortUrlRepo */
$shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class);
$shortUrl = $shortUrlRepo->findOne($shortCode, $domain) ?? -1;
@ -124,6 +171,6 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa
->setParameter('endDate', $dateRange->getEndDate());
}
return $qb;
return [$qb, $shortUrl];
}
}