Что такое «проблема выбора N + 1» в ORM (объектно-реляционное отображение)? - PullRequest
1445 голосов
/ 19 сентября 2008

«Проблема выбора N + 1» обычно указывается как проблема в обсуждениях объектно-реляционного отображения (ORM), и я понимаю, что это связано с необходимостью выполнять множество запросов к базе данных для чего-то, что кажется простым в объектном мире.

У кого-нибудь есть более подробное объяснение проблемы?

Ответы [ 16 ]

885 голосов
/ 19 сентября 2008

Допустим, у вас есть коллекция Car объектов (строк базы данных), и каждый Car имеет коллекцию Wheel объектов (также строк). Другими словами, Car -> Wheel - это отношение 1-ко-многим.

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

SELECT * FROM Cars;

А затем для каждого Car:

SELECT * FROM Wheel WHERE CarId = ?

Другими словами, у вас есть один выбор для автомобилей, а затем N дополнительных выборов, где N - общее количество автомобилей.

В качестве альтернативы можно получить все колеса и выполнить поиск в памяти:

SELECT * FROM Wheel

Это уменьшает количество обращений к базе данных с N + 1 до 2. Большинство инструментов ORM дают вам несколько способов предотвратить выбор N + 1.

Ссылка: Сохранение Java с Hibernate , глава 13.

103 голосов
/ 19 сентября 2008
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Это дает вам набор результатов, где дочерние строки в таблице2 вызывают дублирование, возвращая результаты таблицы1 для каждой дочерней строки в таблице2. Операторы отображения O / R должны дифференцировать экземпляры table1 на основе уникального ключевого поля, а затем использовать все столбцы table2 для заполнения дочерних экземпляров.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N + 1 - это место, где первый запрос заполняет первичный объект, а второй запрос заполняет все дочерние объекты для каждого из возвращенных уникальных первичных объектов.

Рассмотрим:

class House
{
    int Id { get; set; }
    string Address { get; set; }
    Person[] Inhabitants { get; set; }
}

class Person
{
    string Name { get; set; }
    int HouseId { get; set; }
}

и таблицы с похожей структурой. Один запрос по адресу "22 Valley St" может вернуть:

Id Address      Name HouseId
1  22 Valley St Dave 1
1  22 Valley St John 1
1  22 Valley St Mike 1

O / RM должен заполнить экземпляр Home с ID = 1, Address = "22 Valley St", а затем заполнить массив Inhabitants экземплярами People для Dave, John и Mike одним запросом.

Запрос N + 1 для того же адреса, который использовался выше, приведет к:

Id Address
1  22 Valley St

с отдельным запросом типа

SELECT * FROM Person WHERE HouseId = 1

и в результате получается отдельный набор данных, такой как

Name    HouseId
Dave    1
John    1
Mike    1

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

Преимущества одиночного выбора в том, что вы получаете все данные заранее, что может быть именно тем, что вы в конечном итоге желаете. Преимущество N + 1 в том, что сложность запроса снижена, и вы можете использовать отложенную загрузку, когда дочерние наборы результатов загружаются только при первом запросе.

61 голосов
/ 01 декабря 2009

Поставщик, имеющий отношения один-ко-многим с продуктом. Один поставщик имеет (поставляет) много товаров.

***** Table: Supplier *****
+-----+-------------------+
| ID  |       NAME        |
+-----+-------------------+
|  1  |  Supplier Name 1  |
|  2  |  Supplier Name 2  |
|  3  |  Supplier Name 3  |
|  4  |  Supplier Name 4  |
+-----+-------------------+

***** Table: Product *****
+-----+-----------+--------------------+-------+------------+
| ID  |   NAME    |     DESCRIPTION    | PRICE | SUPPLIERID |
+-----+-----------+--------------------+-------+------------+
|1    | Product 1 | Name for Product 1 |  2.0  |     1      |
|2    | Product 2 | Name for Product 2 | 22.0  |     1      |
|3    | Product 3 | Name for Product 3 | 30.0  |     2      |
|4    | Product 4 | Name for Product 4 |  7.0  |     3      |
+-----+-----------+--------------------+-------+------------+

Факторы:

  • Ленивый режим для поставщика установлен на «истина» (по умолчанию)

  • Режим выборки, используемый для запроса по продукту, - Выбрать

  • Режим выборки (по умолчанию): доступ к информации о поставщике

  • Кэширование не играет роли впервые

  • Доступ к поставщику

Режим выборки - Выбрать выборку (по умолчанию)

// It takes Select fetch mode as a default
Query query = session.createQuery( "from Product p");
List list = query.list();
// Supplier is being accessed
displayProductsListWithSupplierName(results);

select ... various field names ... from PRODUCT
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?

Результат:

  • 1 выбор оператора для продукта
  • N выбор операторов для поставщика

Это проблема выбора N + 1!

36 голосов
/ 08 января 2014

Я не могу комментировать другие ответы напрямую, потому что мне не хватает репутации. Но стоит отметить, что проблема, по сути, возникает только потому, что исторически, многие 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, попробуйте еще раз на последних версиях. Вы, вероятно, будете приятно удивлены.

26 голосов
/ 10 июня 2011

Мы переехали из ORM в Джанго из-за этой проблемы. В принципе, если вы попытаетесь сделать

for p in person:
    print p.car.colour

ORM с радостью вернет всех людей (обычно в виде экземпляров объекта Person), но затем потребуется запросить таблицу машин для каждого человека.

Простой и очень эффективный подход к этому - это то, что я называю " fanfolding ", что исключает бессмысленную идею о том, что результаты запроса из реляционной базы данных должны отображаться обратно в исходные таблицы, из которых составлен запрос .

Шаг 1: широкий выбор

  select * from people_car_colour; # this is a view or sql function

Это вернет что-то вроде

  p.id | p.name | p.telno | car.id | car.type | car.colour
  -----+--------+---------+--------+----------+-----------
  2    | jones  | 2145    | 77     | ford     | red
  2    | jones  | 2145    | 1012   | toyota   | blue
  16   | ashby  | 124     | 99     | bmw      | yellow

Шаг 2: Объективизировать

Соси результаты в создателя универсального объекта с аргументом, чтобы разделить после третьего элемента. Это означает, что объект "jones" будет создан не более одного раза.

Шаг 3: Визуализация

for p in people:
    print p.car.colour # no more car queries

См. на этой веб-странице для реализации фанфолдинга для python.

17 голосов
/ 19 сентября 2008

Предположим, у вас есть КОМПАНИЯ и СОТРУДНИК. У КОМПАНИИ много СОТРУДНИКОВ (т. Е. У СОТРУДНИКА есть поле COMPANY_ID).

В некоторых конфигурациях O / R, когда у вас есть сопоставленный объект Company и вы переходите к его объектам Employee, инструмент O / R будет делать один выбор для каждого сотрудника, тогда как если вы просто работали с прямым SQL, может select * from employees where company_id = XX. Таким образом, N (количество сотрудников) плюс 1 (компания)

Так работали начальные версии EJB Entity Beans. Я считаю, что такие вещи, как Hibernate, покончили с этим, но я не слишком уверен. Большинство инструментов, как правило, содержат информацию о своей стратегии отображения.

15 голосов
/ 19 сентября 2008

Вот хорошее описание проблемы - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-lazy

Теперь, когда вы понимаете проблему, ее обычно можно избежать, выполнив выборку соединения в вашем запросе. Это в основном вызывает выборку загруженного объекта с отложенным доступом, поэтому данные извлекаются в одном запросе вместо n + 1 запросов. Надеюсь, это поможет.

13 голосов
/ 26 сентября 2016

Проблема с запросом N + 1 возникает, когда вы забыли выбрать ассоциацию, а затем вам нужно получить к ней доступ:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

LOGGER.info("Loaded {} comments", comments.size());

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

, который генерирует следующие операторы SQL:

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

Сначала Hibernate выполняет запрос JPQL, и получается список PostComment сущностей.

Затем для каждого PostComment соответствующее свойство post используется для создания сообщения журнала, содержащего заголовок Post.

Поскольку ассоциация post не инициализирована, Hibernate должен извлечь объект Post с помощью вторичного запроса, и для N PostComment сущностей будет выполнено еще N запросов (отсюда проблема N + 1 запросов).

Во-первых, вам нужно правильное ведение журнала SQL и мониторинг , чтобы вы могли обнаружить эту проблему.

Во-вторых, такого рода проблемы лучше обнаружить в интеграционных тестах. Вы можете использовать автоматическое утверждение JUnit для проверки ожидаемого количества сгенерированных операторов SQL . Проект db-unit уже предоставляет эту функциональность и является открытым исходным кодом.

Когда вы определили проблему запроса N + 1, , вам нужно использовать FETCH JOIN, чтобы дочерние ассоциации выбирались в одном запросе, а не N . Если вам нужно получить несколько дочерних ассоциаций, лучше выбрать одну коллекцию в начальном запросе, а вторую - вторичный SQL-запрос.

13 голосов
/ 06 июня 2012

Проверьте сообщение Айенде на тему: Борьба с проблемой выбора N + 1 в NHibernate

Как правило, при использовании ORM, такого как NHibernate или EntityFramework, если у вас есть отношение один-ко-многим (master-detail), и вы хотите перечислить все детали для каждой основной записи, вы должны сделать N + 1 запрос обращается к базе данных, где «N» - это число основных записей: 1 запрос, чтобы получить все основные записи, и N запросов, один для каждой основной записи, чтобы получить все данные для основной записи.

Больше запросов к базе данных -> больше времени ожидания -> снижена производительность приложения / базы данных.

Однако у ORM есть варианты, чтобы избежать этой проблемы, в основном используя "соединения".

13 голосов
/ 21 июля 2010

По моему мнению, статья, написанная в Hibernate Pitfall: Почему отношения должны быть ленивыми - это полная противоположность реальной проблеме N + 1.

Если вам нужно правильное объяснение, пожалуйста, обратитесь к Hibernate - Глава 19: Повышение производительности - Выбор стратегии

Выберите выборку (по умолчанию) чрезвычайно уязвимы для N + 1 выбора проблемы, поэтому мы могли бы включить присоединиться к выборке

...