Doctrine Paginator выбирает всю таблицу (очень медленно)?

Это связано с предыдущим вопросом здесь: Doctrine / Symfony query builder add select on left join

Я хочу выполнить сложный запрос соединения с помощью Doctrine ORM. Я хочу выбрать 10 разделенных страниц в блогах, оставив присоединение к одному автору, например значение для текущего пользователя, и хэштеги в сообщении. Мой построитель запросов выглядит так:

$query = $em->createQueryBuilder() ->select('p') ->from('Post', 'p') ->leftJoin('p.author', 'a') ->leftJoin('p.hashtags', 'h') ->leftJoin('p.likes', 'l', 'WITH', 'l.post_id = p.id AND l.user_id = 10') ->where("p.foo = bar") ->addSelect('a AS post_author') ->addSelect('l AS post_liked') ->addSelect('h AS post_hashtags') ->orderBy('p.time', 'DESC') ->setFirstResult(0) ->setMaxResults(10); // FAILS - because left joined hashtag collection breaks LIMITS $result = $query->getQuery()->getResult(); // WORKS - but is extremely slow (count($result) shows over 80,000 rows) $result = new \Doctrine\ORM\Tools\Pagination\Paginator($query, true); 

Как ни странно, count ($ result) в paginator показывает общее количество строк в моей таблице (более 80 000), но пересечение результата $ с результатами foreach. 10 объектов Post, как и ожидалось. Нужно ли мне делать какую-то дополнительную настройку, чтобы правильно ограничивать мой paginator?

Если это ограничение класса paginator, какие у меня есть другие варианты? Написание пользовательского кода paginator или других библиотек paginator?

(бонус): как я могу убрать массив, например $ query-> getQuery () -> getArrayResult () ;?

EDIT: Я оставил бездействие в моей функции. Похоже, что и groupBy, и orderBy вызывает замедление (используя groupBy, а не paginator). Если я опускаю тот или иной, запрос выполняется быстро. Я попытался добавить индекс в столбец «время» в моей таблице, но не видел никаких улучшений.

Вещи, которые я пробовал

 // works, but makes the query about 50x slower $query->groupBy('p.id'); $result = $query->getQuery()->getArrayResult(); // adding an index on the time column (no improvement) indexes: time_idx: columns: [ time ] // the above two solutions don't work because MySQL ORDER BY // ignores indexes if GROUP BY is used on a different column // eg "ORDER BY p.time GROUP BY p.id is" slow 

Вы должны упростить свой запрос. Это сократит время выполнения. Я не могу проверить ваш запрос, но вот несколько указателей:

  • не выполняйте сортировку при выполнении count ()
  • вы можете сортировать по orderBy ('p.id', 'DESC') , индекс будет использоваться
  • вместо leftJoin () вы можете использовать join (), если хотя бы одна запись всегда существует в объединенной таблице. Иначе эта запись пропущена.
  • KNP / Paginator использует DISTINCT () для чтения только отдельных записей, но это может привести к использованию таблицы tmp на диске
  • $ query-> getArrayResult () использует режим hidration массива, который возвращает массив многомерных измерений, и он быстрее, чем объект hidration для большого набора результатов
  • вы можете использовать частичный выбор ('partial p. {id, other used fields}') , таким образом, вы загрузите только нужные поля, возможно, пропустите незавершенные отношения при использовании гидратации объекта
  • проверьте SF profiler EXPLAIN на заданный запрос в разделе доктрины, возможно, индексы не используются
  • p.hashtags и p.likes возвращают только одну строку или одинToMany, который умножает результат
  • возможно, некоторые изменения дизайна сообщений, которые удаляли бы некоторые соединения:
    • имеют поле p.hashtags, определенное как @ORM \ Column (type = "array") и сохраняют строковые значения тегов. Позже возможно использование полнотекстового поиска в сериализованном массиве.
    • имеют поле p.likesCount, определенное как @ORM \ Column (type = "integer"), которое будет иметь количество понравившихся

Я использую KnpLabs / KnpPaginatorBundle и также могу иметь проблемы с скоростью для сложных запросов.

Обычно использование LIMIT x, z медленнее для БД, поскольку оно запускает COUNT для всего набора данных. Если индексы не используются, это очень медленно.

Вы можете использовать другой подход и выполнять некоторые пользовательские разбивки на страницы по продвижению ID, но это усложнит ваш подход. Я использовал это с большими наборами данных, такими как таблицы SYSLOG. Но вы теряете возможности сортировки и общего количества записей.

В конце концов, многие из запросов, используемых в моем приложении, слишком сложны, чтобы правильно использовать Paginator, и я не смог использовать режим гидратации массива с помощью Paginator.

Согласно документации MySQL , ORDER BY не может быть разрешен индексами, если GROUP BY используется в другом столбце. Таким образом, я закончил использование нескольких запросов после обработки, чтобы заполнить мои базовые результаты (ORDERED и LIMITed) отношениями «один ко многим» (например, хэштеги).

Для объединений, которые загружают одну строку из объединенной таблицы, мне удалось присоединиться к желаемым значениям в базовом упорядоченном запросе. Например, при загрузке «как статус» для текущего пользователя необходимо загрузить только один, например, из набора понравившихся, чтобы указать, понравилось или нет текущее сообщение. Точно так же присутствие только одного автора для данного сообщения создает единую объединенную строку автора. например

 $query = $em->createQueryBuilder() ->select('p') ->from('Post', 'p') ->leftJoin('p.author', 'a') ->leftJoin('p.likes', 'l', 'WITH', 'l.post_id = p.id AND l.user_id = 10') ->where("p.foo = bar") ->addSelect('a AS post_author') ->addSelect('l AS post_liked') ->orderBy('p.time', 'DESC') ->setFirstResult(0) ->setMaxResults(10); // SUCCEEDS - because joins only join a single author and single like // no collections are joined, so LIMIT applies only the the posts, as intended $result = $query->getQuery()->getArrayResult(); 

Это дает результат в форме:

 [ [0] => [ ['id'] => 1 ['text'] => 'foo', ['author'] => [ ['id'] => 10, ['username'] => 'username', ], ['likes'] => [ [0] => [ ['post_id'] => 1, ['user_id'] => 10, ] ], ], [1] => [...], ... [9] => [...] ] 

Затем во втором запросе я загружаю хэш-теги для сообщений, загруженных в предыдущий запрос. например

 // we don't care about orders or limits here, we just want all the hashtags $query = $em->createQueryBuilder() ->select('p, h') ->from('Post', 'p') ->leftJoin('p.hashtags', 'h') ->where("p.id IN :post_ids") ->setParameter('post_ids', $pids); 

Что дает следующее:

 [ [0] => [ ['id'] => 1 ['text'] => 'foo', ['hashtags'] => [ [0] => [ ['id'] => 1, ['name'] => '#foo', ], [2] => [ ['id'] => 2, ['name'] => '#bar', ], ... ], ], ... ] 

Затем я просто просматриваю результаты, содержащие хэштеги, и добавляю их к исходным (упорядоченным и ограниченным) результатам. Этот подход заканчивается намного быстрее (хотя он использует больше запросов), поскольку он избегает GROUP BY и COUNT, полностью использует индексы MySQL и позволяет выполнять более сложные запросы, такие как тот, который я опубликовал здесь .