Skip to main content

Record Precounting

Database servers can be slow in counting records, especially when your entity has a million related entities. Doing $collection->count() or count($collection) will be slow because it will do a COUNT() query behind the scenes.

One way to optimize this is to do a precounting and store the count in a separate column. This way, you can get the count of related entities without having to do a very expensive COUNT() query every single time.

However, it would also mean you need to change the way you get the count. To solve this problem, you can decorate the collection object to fetch the count from the field storing the pre-counted value.

The Decorator Class

use Doctrine\Common\Collections\Collection;
use Rekalogika\Collections\Decorator\Decorator\CollectionDecorator;

/**
* @extends CollectionDecorator<array-key,Book>
*/
class BookCollection extends CollectionDecorator
{
/**
* @param Collection<array-key,Book> $collection
*/
public function __construct(
Collection $collection,
private int &$count // pass by reference
) {
parent::__construct($collection);
}

#[\Override]
public function count(): int
{
return $this->count();
}

/**
* Calculates the count and stores it in the `$count` property.
*/
public function preCount(): void
{
$this->count = $this->getWrapped()->count();
}
}

Usage in the one-to-many Side

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity()]
class BookShelf
{
/**
* @var Collection<array-key,Book>
*/
#[ORM\OneToMany(targetEntity: Book::class)]
private Collection $books;

#[ORM\Column()]
private int $booksCount = 0;

public function __construct()
{
$this->books = new ArrayCollection();
}

public function getBooks(): BookCollection
{
return new BookCollection($this->books, $this->booksCount);
}
}

The Caller Side

To get the count, you can do it the same way as before. But instead of asking the database to do that, this time it will give you the value from the pre-counted result instantly:

$count = $bookShelf->getBooks()->count();
// or:
$count = count($bookShelf->getBooks());

When it is time to refresh the pre-counted value, you can do this:

/** @var EntityManagerInterface $entityManager */

$bookShelf->getBooks()->preCount();
$entityManager->flush();