Что такое “Проблема выбора N + 1” в ORM (объектно-реляционное отображение)?

StackOverflow https://stackoverflow.com/questions/97197

  •  01-07-2019
  •  | 
  •  

Вопрос

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

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

Это было полезно?

Решение

Допустим, у вас есть коллекция 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 в режиме гибернации, глава 13.

Другие советы

SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Это дает вам результирующий набор, где дочерние строки в table2 вызывают дублирование, возвращая результаты table1 для каждой дочерней строки в table2.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 идентификатором = 1, Address="22 Valley St", а затем заполнить массив Residents экземплярами 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

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

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

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

***** 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      |
+-----+-----------+--------------------+-------+------------+

Факторы:

  • Отложенный режим для поставщика установлен в значение “true” (по умолчанию)

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

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

  • Кэширование не играет никакой роли в первый раз, когда

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

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

// 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!

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

Итак, краткое содержание таково:Если в прошлом вы избегали joins из-за низкой производительности MySQL при их использовании, попробуйте еще раз в последних версиях.Вы, вероятно, будете приятно удивлены.

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

for p in person:
    print p.car.colour

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

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

Шаг 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.

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

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

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

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

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

На мой взгляд, статья, написанная в Ловушка для впадения в спячку:Почему Отношения Должны быть Ленивыми это прямо противоположно реальной проблеме N + 1.

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

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

Проверьте сообщение Ayende по теме: Устранение проблемы выбора N + 1 В NHibernate

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

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

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

Проблема с запросом 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, вам нужно использовать ВЫБОРКУ СОЕДИНЕНИЯ, чтобы дочерние ассоциации извлекались в одном запросе, а не в N.Если вам нужно получить несколько дочерних ассоциаций, лучше получить одну коллекцию в начальном запросе, а вторую - с помощью вторичного SQL-запроса.

В прилагаемой ссылке приведен очень простой пример проблемы n + 1.Если вы примените его к спящему режиму, это, по сути, будет говорить об одном и том же.Когда вы запрашиваете объект, сущность загружается, но любые ассоциации (если не настроено иное) будут загружены с задержкой.Следовательно, один запрос для корневых объектов и другой запрос для загрузки ассоциаций для каждого из них.Возвращенные 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) раз для получения полезной нагрузки.Таким образом, если индексы помещаются в оперативную память - никаких проблем, это достаточно быстро, потому что задействованы только операции с оперативной памятью.

Намного быстрее выдать 1 запрос, который возвращает 100 результатов, чем выдать 100 запросов, каждый из которых возвращает 1 результат.

Проблема выбора N + 1 - это проблема, и имеет смысл выявлять такие случаи в модульных тестах.Я разработал небольшую библиотеку для проверки количества запросов, выполняемых данным методом тестирования или просто произвольным блоком кода - Сниффер JDBC

Просто добавьте специальное правило JUnit в свой тестовый класс и разместите аннотацию с ожидаемым количеством запросов к вашим тестовым методам:

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}

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

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

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

Сначала вы вставляете идентификаторы своих родительских объектов в виде пакета в таблицу ids.Этот batch_id - это то, что мы генерируем в нашем приложении и сохраняем.

INSERT INTO temp_ids 
    (product_id, batch_id)
    (SELECT p.product_id, ? 
    FROM product p ORDER BY p.product_id
    LIMIT ? OFFSET ?);

Теперь для каждого OneToMany колонка, в которой вы просто делаете SELECT в таблице идентификаторов INNER JOINдобавление дочерней таблицы с помощью WHERE batch_id= (или наоборот).Вы просто хотите убедиться, что вы упорядочиваете по столбцу id, поскольку это упростит объединение столбцов результатов (в противном случае вам понадобится HashMap / таблица для всего набора результатов, что может быть не так уж плохо).

Затем вы просто периодически очищаете таблицу ids.

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

Теперь количество выполняемых вами запросов определяется количеством столбцов OneToMany.

Возьмем, к примеру, Мэтта Солнита, представьте, что вы определяете ассоциацию между автомобилем и Колесами как ЛЕНИВУЮ, и вам нужны несколько полей Wheels.Это означает, что после первого выбора hibernate собирается выполнить "Select * from Wheels where car_id = :id" ДЛЯ КАЖДОГО автомобиля.

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

Чтобы избежать этого, сделайте выборку ассоциации нетерпеливой, чтобы hibernate загружал данные с помощью объединения.

Но обратите внимание, если вы часто не получаете доступа к связанным колесам, лучше отложить это или изменить тип выборки с помощью критериев.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top