1

I am currently trying to get data from a repository to show it in a Bootgrid, but I always exceed the maximum execution time (120 seconds).

I use the following code:

In my Javascript:

$params.table.bootgrid({
    ajax: true,
    url: $params.authoritiesDataPath //the url is correct here, I verified it
})

In my controller:

/**
 * Authorities Data
 *
 * @param Request $request
 * @return JsonResponse
 */
public function dataAction(Request $request)
{
    $this->denyAccessUnlessGranted(RoleVoterHelper::SECTION_COMPANY_VIEW);
    try {
        $data = $this->getDoctrine()->getRepository('BugTrackerModelBundle:Authority')->findByParameters(
            $request->request->all()
        );
    } catch (\Exception $e) {
        $data = [
            'status'   => 'error',
            'error'    => $e->getMessage(),
            'rows'     => [],
            'current'  => 1,
            'rowCount' => 0,
            'total'    => 0,
        ];
    }

    return new JsonResponse($data);
}

in my repository:

public function findByParameters(array $parameters)
{
    $queryBuilder = $this->createQueryBuilder('a')
        ->select('COUNT(a.id)');

    if (!empty($parameters['searchPhrase'])) {
        $queryBuilder->where('a.name LIKE :search')
            ->setParameter('search', '%'.$parameters['searchPhrase'].'%');
    }

    $parameters['rows'] = array();
    $parameters['current'] = isset($parameters['current']) ? (int)$parameters['current'] : 0;
    if ($parameters['total'] = (int)$queryBuilder->getQuery()->getSingleScalarResult()) {
        $queryBuilder->select('a.id', 'a.name', 'COUNT(DISTINCT c.id) as companies',
            'COUNT(DISTINCT u.id) as users', 'COUNT(DISTINCT dl.id) as deviceLists',
            'COUNT(DISTINCT d.id) as devices', 'a.name as authority', 'a.enabled')
            ->leftJoin('BugTrackerModelBundle:Company', 'c', Join::WITH, 'c.authority = a.id')
            ->leftJoin('BugTrackerModelBundle:Device', 'd', Join::WITH, 'd.authority = a.id')
            ->leftJoin('BugTrackerModelBundle:Device\DeviceList', 'dl', Join::WITH, 'dl.authority = a.id')
            ->leftJoin('BugTrackerModelBundle:User', 'u', Join::WITH, 'u.company = c.id')
            ->groupBy('a.id');

        if (!empty($parameters['sort'])) {
            $order = reset($parameters['sort']) ?: 'ASC';
            switch (key($parameters['sort'])) {
                case 'name':
                    $queryBuilder->orderBy('c.name', $order);
                    break;
                case 'users':
                    $queryBuilder->orderBy('users', $order);
                    break;
                case 'company':
                    $queryBuilder->orderBy('companies', $order);
                    break;
                case 'enabled':
                    $queryBuilder->orderBy('a.enabled', $order);
                    break;
                default:
                    $queryBuilder->orderBy('c.id', $order);
            }
        }

        if (isset($parameters['rowCount']) && $parameters['rowCount'] > 0) {
            $queryBuilder->setFirstResult(($parameters['current'] - 1) * $parameters['rowCount'])
                ->setMaxResults($parameters['rowCount']);
        }

        $parameters['rows'] = $queryBuilder->getQuery()->getArrayResult();
    }

    return $parameters;
}

I tried returning arrays pretty much everywhere to find where the loop was (I'm guessing that it's a loop since my code is usually very fast), and it seems to come from the following line

$parameters['rows'] = $queryBuilder->getQuery()->getArrayResult();

I tried returning $queryBuilder->getQuery() and it took a few seconds, so the issue is with getArrayResult

The query it returned when I printed the sql:

SELECT a0_.id AS id_0, a0_.name AS name_1, COUNT(DISTINCT c1_.id) AS sclr_2, COUNT(DISTINCT u2_.id) AS sclr_3, COUNT(DISTINCT d3_.id) AS sclr_4, COUNT(DISTINCT d4_.id) AS sclr_5, a0_.name AS name_6, a0_.enabled AS enabled_7 FROM authority a0_ LEFT JOIN client_company c1_ ON (c1_.authority_id = a0_.id) LEFT JOIN device d4_ ON (d4_.authority_id = a0_.id) LEFT JOIN device_list d3_ ON (d3_.authority_id = a0_.id) LEFT JOIN user u2_ ON (u2_.company_id = c1_.id) GROUP BY a0_.id LIMIT 5 OFFSET 0

Here is the "explain" when I ran the query in PhpMyAdmin: Explain

I really don't understand why it takes so much time, I never had issues with getting data from my database and there is no loop in all that code that could cause it. Is there any way for me to test other things to understand why it takes so much time and change it?

Liora Haydont
  • 1,252
  • 1
  • 12
  • 25

2 Answers2

1

have you indexed the correct columns? this should reduce the querying time, additionally, it could be that the results set is so complex it's taking a while to hydrate the array which is causing the time duration you're seeing.

Andy
  • 679
  • 2
  • 10
  • 25
  • All the column I'm using from other tables are indexed but it still takes almost a minute to run. – Liora Haydont Nov 29 '17 at 18:37
  • 1
    try and get the SQL to print, should be witin the profiler, run that in your SQL program with explain. So: explain [query] this should help you see if some of the indexes are being missed. Also, the query takes a while or the hydration does? If it's the hydration, have you considered breaking out the query into multiple queries and then hydrate your own array from the results? – Andy Nov 30 '17 at 10:15
  • I ran the SQL in PhpMyAdmin and it is slow so I don't think the issue comes from the hydratation. I put a screenshot of the explain in the question. – Liora Haydont Nov 30 '17 at 16:47
0

Looking at the query produced, you have 4 joins in it and it hydrates 5 entities BugTrackerModelBundle:Authority, BugTrackerModelBundle:Company, BugTrackerModelBundle:Device, BugTrackerModelBundle:Device\DeviceList and BugTrackerModelBundle:User.

That `s a lot of joins,as "the process of hydration becomes extremely expensive when more than 2 LEFT JOIN operations clauses". See Reference. In short, the result set is large and doctrine takes too long to map it to Entities.

My assumption is that the ORM is taking too long to normalize the result set returned by the query.

My advice is to split the query to 2 with maximum 2 joins each:

$queryBuilder = $this->createQueryBuilder('a')
            ->select('a.id', 'a.name', 
            'COUNT(DISTINCT c.id) as companies',
            'COUNT(DISTINCT u.id) as users',
            'a.name as authority', 'a.enabled')
            ->leftJoin('BugTrackerModelBundle:Company', 'c', Join::WITH, 'c.authority = a.id')
            ->leftJoin('BugTrackerModelBundle:User', 'u', Join::WITH, 'u.company = c.id')
            ->groupBy('a.id');

$queryBuilder = $this->createQueryBuilder('a')
            ->select('a.id', 'a.name',
            'COUNT(DISTINCT dl.id) as deviceLists',
            'COUNT(DISTINCT d.id) as devices', 'a.name as authority', 'a.enabled')
            ->leftJoin('BugTrackerModelBundle:Device', 'd', Join::WITH, 'd.authority = a.id')
            ->leftJoin('BugTrackerModelBundle:Device\DeviceList', 'dl', Join::WITH, 'dl.authority = a.id')
            ->groupBy('a.id');

Hope this helps.

Jannes Botis
  • 11,154
  • 3
  • 21
  • 39
  • Thank you! I tried running with your example and it helped a lot (went from a minute and a half to 17 sec approx). Sadly, it is still to slow for me, since the webpage should only take a few seconds to load. I'll keep looking. – Liora Haydont Dec 01 '17 at 20:47
  • Ah, turns out I needed to create a new query builder for every query or the joins just added up and the results were the same. Now that I did so it only takes 1.5 second! Thank you! – Liora Haydont Dec 01 '17 at 21:11