Статьи

Оптимизация запросов Neo4j Cypher

На прошлой неделе я потратил немало часов, пытаясь оптимизировать около 20 запросов Cypher, которые выполнялись катастрофически (от 36866 мс до 155575 мс) с данными из работающей системы. После некоторых проб и ошибок, а также большого вклада Майкла , я смог в целом выяснить, что нужно сделать с запросами, чтобы заставить их работать лучше — в конце этого самый худший запрос упал до 521 мс на холодный график и 1 Гб кучи (и этот запрос имеет необязательные пути — не совсем уверен, как это улучшить), а остальные были менее 50 мс — довольно значительное улучшение по сравнению с исходными числами.

Надеясь, что это могло бы помочь кому-то еще, вот что я сделал (в основном догадываюсь о работе и в основном ненаучно) — возможно, Майкл может помочь объяснить внутренности и исправить меня в некоторых предположениях. Первое, что я сделал, — убедился, что каждый запрос шифра использует параметры — как указано в документации Neo4j , это помогает в кэшировании планов выполнения.

Во-вторых, я наткнулся на сообщение в списке рассылки Neo4j, в котором Майкл упомянул НЕ повторное создание экземпляра ExecutionEngine, чтобы вышеупомянутые параметризованные запросы действительно кэшировали. Многим это может показаться очевидным, но это факт, который легко ускользнуть, учитывая, что у меня есть класс с именем QueryExecutor, который содержит метод для выполнения запроса с картой параметров, и метод создал новый ExecutionEngine для каждого запроса. Как только этот метод был написан и использовался много-много раз, об этом было очень легко забыть. Тем не менее, это было очень важным фактором в общей производительности (упоминание в документации было бы чрезвычайно полезно), и это объясняло, почему мои запросы обычно выполнялись в одно и то же время, даже когда они были параметризованы. Изменение этого параметра для использования кэшированного ExecutionEngine привело к тому, что нижняя половина моего рабочего листа времени запроса упала … от 0 до 1 мс после кэширования — отличный прогресс.

Теперь перейдем к каждому запросу, начиная с худшего. Я решил оптимизировать на моей локальной машине, с выделенным пространством кучи только 1 ГБ, и на холодном графике. Поэтому я игнорирую улучшение выполнения запроса после кэширования — я считаю, что это лучший способ обеспечить прогресс — если первое попадание запроса не улучшается, то вы действительно его не оптимизировали. Таким образом, если он действительно хорошо работает с ограниченным аппаратным обеспечением, я уверен, что он будет работать лучше на производстве.

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

Мой первый запрос составлял в среднем около 76558 мс (время было получено, если ввести время начала и окончания метода выполнения двигателя). После первой оптимизации она снизилась до 466 мс. Вот оригинальный запрос: https://gist.github.com/4436272

И это оптимизированный вариант: https://gist.github.com/4436281 Не нужно было выполнять это огромное совпадение только для того, чтобы отфильтровать результаты на основе свойства alertDate для a, поэтому я уменьшил совпадение, чтобы получить наименьший набор данных, которые могут быть отфильтрованы в первую очередь, т.е. путь к.
Если бы вы выполнили неверный запрос до первого совпадения, вы бы увидели что-то вроде 20К строк. После фильтрации они представляют собой 450 нечетных строк. Как вы можете себе представить, второй запрос быстро сокращает количество результатов, с которыми вы потенциально работаете.

Второе изменение было тем, что я узнал от Майкла в тот день, когда я спросил, имеет ли смысл проводить гигантский матч или продолжать сокращать с помощью подзапросов. Он ответил: «Вопрос в том, используете ли вы совпадение для фактического расширения результирующего набора путем описания шаблонов, или вы просто хотите убедиться, что некоторые отношения существуют (или нет), иначе говоря, FILTERING. Если последнее, вы можете использовать синтаксис выражения пути в предложении where.

1
2
WHERE a-[:FOO]->b
or WHERE NOT(a-[:FOO]->b)'

Это заняло некоторое привыкание, так как способ, которым я написал предложение MATCH, был именно таким, каким я думал об этом в своей голове, — но теперь я могу различить совпадение результатов, требуемых, и совпадение с фильтром. В приведенном выше запросе мне нужно (ir) в моих результатах, поэтому нет необходимости включать (a) — [: alert_for_inspection] -> (i) в совпадение; Я могу просто использовать его в ГДЕ, чтобы убедиться, что a действительно относится к i.

Вот еще один пример: https://gist.github.com/4436293

Сразу видно, что мы фильтруем отношения cm по дате — нам не нужно сначала идти и сопоставлять все виды вещей, если они не попадают в диапазон дат. Так что эту часть запроса можно переписать так:

1
2
3
4
5
6
start c=node:companies(id={id})
match c <-[parent_company*0..]-(n)
 
with n
match n-[:defines_business_process]->(bp)-[:has_cycle]->(cm)
where cm.measureDate>={measureStartDate} and cm.measureDate<={measureEndDate}

После этого следующий фильтр действует по тому же принципу:

1
2
with cm
match (cm)-[:cycle_metric] ->m-[:metric_activity] ->ma-[:metric_unit] -> (u)-[:alert_for_unit]-(a) where a.alertDate=cm.measureDate and a.fromEntityType={type}

Это дополнительно сокращает наш набор результатов. Наконец, добавьте соединения к r (для наших результатов), убедившись, что пути, которые не приводят к r, но необходимы, входят в предложение WHERE:

1
2
3
4
with a,ma
match (r) < - [:for_inspection_result]-a-[:alert_for_inspection]- > i
where (i) < -[:metric_inspection]-(ma)
return a.id as alertId, r.value as resultValue

Вот полный запрос: https://gist.github.com/4436328 Исходное время — 33360мс, после оптимизации — 246мс.

По крайней мере, для меня большинство моих запросов соответствовало этой схеме, поэтому ко 2-му дню я смог очень быстро их реорганизовать. Интересно то, что после этого я все еще чувствовал вялый ответ, но время запроса, напечатанное в моем журнале, было крошечным. После исключения я обнаружил, что мой код на самом деле застрял в течение очень долгого времени после выполнения запроса (путем executeEngine.execute), но в течение первой итерации набора результатов. Я предполагаю, что результаты не обязательно собираются во время выполнения метода execute () , но лениво извлекается при итерации набора результатов — я не знаю, что такое Cypher, поэтому могу ошибаться. Но выбор времени итерации сам по себе указывает на еще более плохо написанные запросы.

Остальные фрагменты — ORDER BY добавляют много времени. Если вы можете обойтись без этого, это первое, что вы должны бросить. DISTINCT также добавляет время, но во многих моих случаях его было трудно отбросить.

Если вы хотите проверить отсутствие необязательных путей, где я обычно делаю MATCH (u) — [r?: Has_a] — (a) WHERE NOT (r равно нулю), вместо этого переписать как MATCH (u) — [ other_stuff] — .. WHERE NOT (u — [: has_a] -a) и это работает намного лучше. Однако, где у меня есть дополнительные пути, такие как MATCH X- [o?: Необязательный] -Y ГДЕ (o присутствует, соответствует Y для A и B) ИЛИ (o отсутствует, соответствует X для c и d), я не смог упростить и эти запросы все еще занимают некоторое время по сравнению с другими без дополнительных путей.

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

Итак, подведем итог:

  1. Всегда параметризируйте свои запросы
  2. Кэшируйте ExecutionEngine
  3. Выясните, что вы фильтруете, и примените этот фильтр как можно раньше с наименьшим возможным соответствием, чтобы ваш набор результатов постепенно уменьшался по мере продвижения в запросе. Продолжайте измерять время и результаты, возвращаемые в каждом подзапросе, чтобы вы могли решить, что будет первым, если фильтр неочевиден
  4. Изучая ваши предложения MATCH и RETURN — включите в MATCH только те части, которые требуются в RETURN. Остальные, которые будут фильтровать результаты, могут перейти в ГДЕ
  5. Если вам не нужен ORDER BY, бросьте его вчера
  6. Если вам не нужен DISTINCT, избавьтесь от него тоже
  7. Проверка отсутствия / наличия необязательных путей может быть перемещена из МАТЧ в WHERE, если вам не нужны навороченные вещи фильтрации на основе этого
  8. Время не только execute () в запросе, но и время, чтобы перебрать результаты
  9. Если ваша консоль webadmin зависла, вы сделали плохую вещь. Оставьте различные части вашего запроса, чтобы выяснить, кто обидел.
  10. Попробуйте использовать живые данные, насколько это возможно
  11. Протестируйте холодный график с скупыми ресурсами — вы почувствуете себя намного лучше, когда увидите, как он проносится мимо вас на производстве!

Ссылка: Оптимизация запросов Neo4j Cypher от нашего партнера JCG Алдрина и Луанны Мискитта в блоге Thought Bytes .