«Проблема выбора N + 1» обычно указывается как проблема в обсуждениях объектно-реляционного отображения (ORM), и я понимаю, что это связано с необходимостью выполнять множество запросов к базе данных для чего-то, что кажется простым в объекте Мир.
У кого-нибудь есть более подробное объяснение проблемы?
orm
select-n-plus-1
Ларс А. Бреккен
источник
источник
Ответы:
Допустим, у вас есть коллекция
Car
объектов (строк базы данных), и у каждогоCar
есть коллекцияWheel
объектов (также строк). Другими словами,Car
→Wheel
это отношение 1-ко-многим.Теперь предположим, что вам нужно пройтись по всем машинам, и для каждого из них распечатать список колес. Наивная реализация O / R сделала бы следующее:
И тогда для каждого
Car
:Другими словами, у вас есть один выбор для автомобилей, а затем N дополнительных выборов, где N - общее количество автомобилей.
В качестве альтернативы можно получить все колеса и выполнить поиск в памяти:
Это сокращает число обращений к базе данных с N + 1 до 2. Большинство инструментов ORM предоставляют несколько способов предотвратить выбор N + 1.
Ссылка: Java Persistence с Hibernate , глава 13.
источник
SELECT * from Wheel;
) вместо N + 1. С большим N, производительность может быть очень значительным.Это дает вам набор результатов, где дочерние строки в table2 вызывают дублирование, возвращая результаты table1 для каждой дочерней строки в table2. Операторы сопоставления O / R должны дифференцировать экземпляры table1 на основе уникального ключевого поля, а затем использовать все столбцы table2 для заполнения дочерних экземпляров.
N + 1 - это место, где первый запрос заполняет первичный объект, а второй запрос заполняет все дочерние объекты для каждого из возвращенных уникальных первичных объектов.
Рассматривать:
и таблицы с похожей структурой. Один запрос по адресу "22 Valley St" может вернуть:
O / RM должен заполнить экземпляр Home с ID = 1, Address = "22 Valley St", а затем заполнить массив Inhabitants экземплярами People для Dave, John и Mike одним запросом.
Запрос N + 1 для того же адреса, который использовался выше, приведет к:
с отдельным запросом, как
и в результате в отдельный набор данных, как
и окончательный результат будет таким же, как указано выше с одним запросом.
Преимущества единого выбора в том, что вы получаете все данные заранее, что может быть именно тем, что вы в конечном итоге желаете. Преимущество N + 1 в том, что сложность запроса снижена, и вы можете использовать отложенную загрузку, когда дочерние наборы результатов загружаются только при первом запросе.
источник
Поставщик, имеющий отношения один-ко-многим с продуктом. Один поставщик имеет (поставляет) много товаров.
Факторы:
Ленивый режим для поставщика установлен на «истина» (по умолчанию)
Режим выборки, используемый для запроса по продукту, - Выбор.
Режим выборки (по умолчанию): доступ к информации о поставщике
Кэширование не играет роли впервые
Доступ к поставщику
Режим выборки - «Выбрать выборку» (по умолчанию)
Результат:
Это проблема выбора N + 1!
источник
Я не могу комментировать другие ответы напрямую, потому что мне не хватает репутации. Но стоит отметить, что проблема, по сути, возникает только потому, что исторически, многие dbms были достаточно плохими, когда дело доходит до обработки соединений (MySQL является особенно заслуживающим внимания примером). Таким образом, n + 1 часто был значительно быстрее соединения. И тогда есть способы улучшить n + 1, но все еще без необходимости объединения, к чему относится исходная проблема.
Тем не менее, MySQL теперь намного лучше, чем раньше, когда дело доходит до объединений. Когда я впервые изучил MySQL, я часто использовал соединения. Затем я обнаружил, насколько они медленные, и вместо этого переключился на n + 1 в коде. Но недавно я вернулся к объединениям, потому что MySQL теперь намного лучше справляется с ними, чем когда я впервые начал его использовать.
В наши дни простое объединение с правильно проиндексированным набором таблиц редко является проблемой с точки зрения производительности. И если это дает снижение производительности, то использование подсказок индекса часто решает их.
Это обсуждается здесь одним из разработчиков MySQL:
http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html
Итак, подведем итоги: если в прошлом вы избегали объединений из-за ужасной производительности MySQL, попробуйте еще раз последние версии. Вы, вероятно, будете приятно удивлены.
источник
JOIN
алгоритмов, используемых в СУБД, называется вложенными циклами. Это принципиально N + 1 выбор под капотом. Единственное отличие состоит в том, что БД сделала разумный выбор, чтобы использовать ее на основе статистики и индексов, а не кода клиента, форсирующего этот путь категорически.Из-за этой проблемы мы отошли от ORM в Джанго. В принципе, если вы попытаетесь сделать
ORM с радостью вернет всех людей (обычно в виде экземпляров объекта Person), но затем потребуется запросить таблицу автомобилей для каждого человека.
Простой и очень эффективный подход к этому - это то, что я называю « фанфолдингом », что позволяет избежать бессмысленной идеи, согласно которой результаты запроса из реляционной базы данных должны отображаться обратно в исходные таблицы, из которых составлен запрос.
Шаг 1: Широкий выбор
Это вернет что-то вроде
Шаг 2: Объективировать
Соси результаты в создателя универсального объекта с аргументом, чтобы разделить после третьего элемента. Это означает, что объект "jones" будет создан не более одного раза.
Шаг 3: Визуализация
Смотрите эту веб-страницу для реализации фанфолдинга для Python.
источник
select_related
, что призвано решить эту проблему - на самом деле, его документы начинаются с примера, аналогичного вашемуp.car.colour
примеру.select_related()
иprefetch_related()
в Джанго сейчас.select_related()
и друг, похоже, не делает каких-либо явно полезных экстраполяций объединения, таких какLEFT OUTER JOIN
. Проблема не в интерфейсе, а в странной идее о том, что объекты и реляционные данные сопоставимы ... на мой взгляд.В чем проблема N + 1?
Проблема запроса N + 1 возникает, когда структура доступа к данным выполнила N дополнительных операторов SQL для извлечения тех же данных, которые могли быть получены при выполнении основного запроса SQL.
Чем больше значение N, тем больше запросов будет выполнено, тем больше влияние на производительность. И, в отличие от медленного журнала запросов, который может помочь вам найти медленные запросы, проблема N + 1 не будет обнаружена, потому что каждый отдельный дополнительный запрос выполняется достаточно быстро, чтобы не вызывать медленный журнал запросов.
Проблема заключается в выполнении большого количества дополнительных запросов, которые в целом требуют достаточного времени для замедления времени отклика.
Давайте рассмотрим, что у нас есть следующие таблицы базы данных post и post_comments, которые образуют отношение таблицы «один ко многим» :
Мы собираемся создать следующие 4
post
строки:И мы также создадим 4
post_comment
дочерние записи:Проблема запроса N + 1 с простым SQL
Если вы выбрали
post_comments
использование этого SQL-запроса:И позже вы решаете получить связанный
post
title
с каждымpost_comment
:Вы собираетесь вызвать проблему запроса N + 1, потому что вместо одного запроса SQL вы выполнили 5 (1 + 4):
Исправить проблему с запросом N + 1 очень просто. Все, что вам нужно сделать, это извлечь все данные, которые вам нужны в исходном запросе SQL, например:
На этот раз выполняется только один SQL-запрос для извлечения всех данных, которые нам еще интересны.
Проблема запроса N + 1 с JPA и Hibernate
При использовании JPA и Hibernate есть несколько способов вызвать проблему с запросом N + 1, поэтому очень важно знать, как можно избежать этих ситуаций.
В течение следующих примеров мы рассмотрим КАРТОГРАФИРОВАНИЕ
post
иpost_comments
таблиц для следующих лиц:Отображения JPA выглядят так:
FetchType.EAGER
Использование
FetchType.EAGER
неявного или явного для ваших ассоциаций JPA - плохая идея, потому что вы собираетесь получать больше данных, которые вам нужны. Более того,FetchType.EAGER
стратегия также подвержена проблемам с N + 1 запросами.К сожалению,
@ManyToOne
и@OneToOne
ассоциации используютFetchType.EAGER
по умолчанию, поэтому , если ваши отображения выглядеть следующим образом :Вы используете
FetchType.EAGER
стратегию и каждый раз, когда вы забываете использовать ееJOIN FETCH
при загрузке некоторыхPostComment
сущностей с помощью запроса API JPQL или Criteria:Вы собираетесь вызвать проблему запроса N + 1:
Обратите внимание на дополнительном ЗЕЬЕСТ, которые выполняются , потому что
post
ассоциация должна быть извлечены до возвращенияList
изPostComment
субъектов.В отличие от плана выборки по умолчанию, который вы используете при вызове
find
методаEnrityManager
, запрос API-интерфейса JPQL или Criteria определяет явный план, который Hibernate не может изменить, внедрив FETCH JOIN автоматически. Итак, вам нужно сделать это вручную.Если вам вообще не нужна
post
ассоциация, вам не повезло при использовании,FetchType.EAGER
потому что нет способа избежать ее получения. Вот почему лучше использоватьFetchType.LAZY
по умолчанию.Но, если вы хотите использовать
post
ассоциацию, вы можете использовать ееJOIN FETCH
для решения проблемы N + 1:На этот раз Hibernate выполнит одну инструкцию SQL:
FetchType.LAZY
Даже если вы переключитесь на использование
FetchType.LAZY
явно для всех ассоциаций, вы все равно можете столкнуться с проблемой N + 1.На этот раз
post
ассоциация отображается так:Теперь, когда вы выбираете
PostComment
объекты:Hibernate выполнит одну инструкцию SQL:
Но, если потом, вы будете ссылаться на ленивую
post
ассоциацию:Вы получите вопрос N + 1:
Поскольку
post
связь извлекается лениво, вторичный оператор SQL будет выполняться при доступе к ленивой ассоциации, чтобы построить сообщение журнала.Опять же, исправление состоит в добавлении
JOIN FETCH
предложения к запросу JPQL:И, как и в
FetchType.EAGER
примере, этот запрос JPQL будет генерировать один оператор SQL.Как автоматически определить проблему с запросом N + 1
Если вы хотите автоматически обнаружить проблему с запросом N + 1 на уровне доступа к данным, в этой статье объясняется, как это можно сделать с помощью проекта с
db-util
открытым исходным кодом.Во-первых, вам нужно добавить следующую зависимость Maven:
После этого вам просто нужно использовать
SQLStatementCountValidator
утилиту для утверждения базовых операторов SQL, которые генерируются:Если вы используете
FetchType.EAGER
и запускаете приведенный выше тестовый пример, вы получите следующий тестовый случай:источник
SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5
. Но вы получаете 2 машины с 5 колесами (первый автомобиль со всеми 4 колесами и второй автомобиль только с 1 колесом), потому что LIMIT ограничит весь набор результатов, а не только корневой пункт.Предположим, у вас есть КОМПАНИЯ и СОТРУДНИК. У КОМПАНИИ много СОТРУДНИКОВ (т. Е. У СОТРУДНИКА есть поле COMPANY_ID).
В некоторых конфигурациях O / R, когда у вас есть сопоставленный объект Company и вы переходите к его объектам Employee, инструмент O / R будет делать один выбор для каждого сотрудника, тогда как, если вы просто работали с простым SQL, вы могли бы
select * from employees where company_id = XX
. Таким образом, N (количество сотрудников) плюс 1 (компания)Вот как работали начальные версии EJB Entity Beans. Я считаю, что такие вещи, как Hibernate, покончили с этим, но я не слишком уверен. Большинство инструментов, как правило, содержат информацию о своей стратегии отображения.
источник
Вот хорошее описание проблемы
Теперь, когда вы понимаете проблему, ее обычно можно избежать, выполнив выборку соединения в вашем запросе. Это в основном вызывает выборку загруженного объекта с отложенным доступом, поэтому данные извлекаются в одном запросе вместо n + 1 запросов. Надеюсь это поможет.
источник
Посмотрите сообщение Ayende на тему: Борьба с проблемой N + 1 в NHibernate .
По сути, при использовании ORM, например NHibernate или EntityFramework, если у вас есть отношение «один ко многим» (master-detail), и вы хотите перечислить все детали для каждой основной записи, вы должны сделать N + 1 запросов на вызов к база данных, где «N» - это число основных записей: 1 запрос для получения всех основных записей и N запросов, по одному для каждой основной записи, для получения всех подробностей для основной записи.
Больше запросов к базе данных → больше времени ожидания → снижается производительность приложения / базы данных.
Однако у ORM есть варианты, чтобы избежать этой проблемы, в основном используя JOIN.
источник
Гораздо быстрее выдать 1 запрос, который возвращает 100 результатов, чем выдать 100 запросов, каждый из которых возвращает 1 результат.
источник
На мой взгляд, статья, написанная в Hibernate Pitfall: Почему отношения должны быть ленивыми , прямо противоположна реальной проблеме N + 1.
Если вам нужно правильное объяснение, пожалуйста, обратитесь к Hibernate - Глава 19: Повышение производительности - Выбор стратегий
источник
Приведенная ссылка имеет очень простой пример проблемы n + 1. Если вы примените его к Hibernate, то это в основном говорит об одном и том же. Когда вы запрашиваете объект, объект загружается, но любые ассоциации (если не указано иное) будут загружаться с отложенной загрузкой. Отсюда один запрос для корневых объектов и другой запрос для загрузки ассоциаций для каждого из них. 100 возвращенных объектов означают один начальный запрос, а затем 100 дополнительных запросов для получения ассоциации для каждого, n + 1.
http://pramatr.com/2009/02/05/sql-n-1-selects-explained/
источник
У одного миллионера N машин. Вы хотите получить все (4) колеса.
Один (1) запрос загружает все автомобили, но для каждого (N) автомобиля отправляется отдельный запрос на загрузку колес.
Расходы:
Предположим, что индексы вписываются в оперативную память.
Разбор и планирование запросов 1 + N + поиск по индексу И доступ к табличке 1 + N + (N * 4) для загрузки полезной нагрузки.
Предположим, что индексы не вписываются в оперативную память.
Дополнительные расходы в худшем случае 1 + N доступ к пластине для индекса загрузки.
Резюме
Горлышко бутылки - это доступ к платформе (около 70 раз в секунду при произвольном доступе по жесткому диску). При активном выборе соединения можно также получить доступ к платформе 1 + N + (N * 4) раз для полезной нагрузки. Так что, если индексы вписываются в оперативную память - нет проблем, это достаточно быстро, потому что задействованы только оперативные памяти.
источник
Проблема выбора N + 1 - это боль, и имеет смысл выявлять такие случаи в модульных тестах. Я разработал небольшую библиотеку для проверки количества запросов, выполняемых данным методом тестирования или просто произвольным блоком кода - JDBC Sniffer
Просто добавьте специальное правило JUnit в ваш тестовый класс и поместите аннотацию с ожидаемым количеством запросов к вашим тестовым методам:
источник
Проблема, как говорили другие более элегантно, заключается в том, что у вас либо декартово произведение столбцов OneToMany, либо вы выполняете N + 1 выбор. Возможен либо гигантский набор результатов, либо общение с базой данных соответственно.
Я удивлен, что это не упомянуто, но так я обошел эту проблему ... Я делаю таблицу временных идентификаторов . Я также делаю это, когда у вас есть
IN ()
ограничение пункта .Это не работает для всех случаев (возможно, даже не для большинства), но особенно хорошо работает, если у вас много дочерних объектов, так что декартово произведение выйдет из-под контроля (т.е. много
OneToMany
столбцов, число результатов будет умножение столбцов) и его более пакетной работы.Сначала вы вставляете идентификаторы родительского объекта в виде пакета в таблицу идентификаторов. Этот batch_id - это то, что мы генерируем в нашем приложении и удерживаем.
Теперь для каждого
OneToMany
столбца вы просто делаетеSELECT
в таблице идентификаторовINNER JOIN
дочернюю таблицу сWHERE batch_id=
(или наоборот). Вы просто хотите убедиться, что вы упорядочиваете по столбцу id, поскольку это упростит объединение столбцов результатов (в противном случае вам понадобится HashMap / Table для всего набора результатов, что может быть не так уж плохо).Тогда вы просто периодически очищаете таблицу идентификаторов.
Это также работает особенно хорошо, если пользователь выбирает, скажем, 100 или около того отдельных элементов для некоторой массовой обработки. Поместите 100 различных идентификаторов во временную таблицу.
Теперь количество запросов, которые вы делаете, зависит от количества столбцов OneToMany.
источник
Возьмите пример Matt Solnit, представьте, что вы определяете связь между Car и Wheels как LAZY, и вам нужны некоторые поля Wheels. Это означает, что после первого выбора, hibernate будет делать «Выбрать * из колес, где car_id =: id» ДЛЯ КАЖДОГО автомобиля.
Это делает первый выбор и более 1 выбор на каждую N машину, поэтому это называется проблемой n + 1.
Чтобы избежать этого, заставьте ассоциацию извлекаться как активную, чтобы hibernate загружал данные с объединением.
Но обратите внимание, если много раз вы не получаете доступ к связанным колесам, лучше оставить их LAZY или изменить тип выборки с помощью Criteria.
источник