Что такое N+1 Выберите вопрос запроса?



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



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

1109   16  

16 ответов:

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

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

SELECT * FROM Cars;

а то для каждого Car:

SELECT * FROM Wheel WHERE CarId = ?

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

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

SELECT * FROM Wheel

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

ссылки: Java Persistence with Hibernate Глава 13.

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

это дает вам результирующий набор, где дочерние строки в table2 вызывают дублирование, возвращая результаты table1 для каждой дочерней строки в table2. O / R mappers должны различать экземпляры 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", а затем заполнить массив жителей экземплярами 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-сложность запроса снижается, и вы можете использовать ленивую загрузку, когда дочерние результирующие наборы загружаются только по первому запросу.

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

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

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

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

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

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

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

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

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

for p in person:
    print p.car.colour

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

простой и очень эффективный подход к этому-это то, что я называю "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: Objectify

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

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

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

посмотреть этой странице для реализации fanfolding для python.

Предположим, у вас есть компания и сотрудник. Компания имеет много сотрудников (т. е. сотрудник имеет поле COMPANY_ID).

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

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

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

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

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

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

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

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

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

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

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

поставляемая ссылка имеет очень простой пример проблемы n + 1. Если вы примените его к Hibernate, это в основном говорит об одном и том же. Когда вы запрашиваете объект, сущность загружается, но любые ассоциации (если не настроено иначе) будут загружены с задержкой. Следовательно, один запрос для корневых объектов и другой запрос для загрузки ассоциаций для каждого из них. 100 возвращенных объектов означает один начальный запрос, а затем 100 дополнительных запросов для получения ассоциации для каждого, n + 1.

http://pramatr.com/2009/02/05/sql-n-1-selects-explained/

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

проблема с запросом 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 название.

потому что

один миллионер имеет N автомобилей. Вы хотите получить все (4) колеса.

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

затраты:

предположим, что индексы вписываются в ОЗУ.

1 + N разбор запросов и строгание + поиск индекса и 1 + N + (N * 4) доступ к пластине для загрузки полезной нагрузки.

предположим, что индексы не вписываются в оперативную память.

дополнительные расходы в худшем случае 1 + N пластины доступы для загрузки индекса.

резюме

горлышко бутылки имеет доступ к плите (ок. 70 раз в секунду произвольный доступ на hdd) Нетерпеливый выбор соединения также получит доступ к пластине 1 + N + (N * 4) раз для полезной нагрузки. Так что если индексы вписываются в ОЗУ-нет проблем, его достаточно быстро, потому что только операции ОЗУ участвуют.

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

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

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

сначала вы вставляете идентификаторы родительских объектов в виде пакета в таблицу идентификаторов. Этот 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 JOINing дочерняя таблица с WHERE batch_id= (или наоборот). Вы просто хотите убедиться, что вы заказываете столбец id, так как это упростит объединение столбцов результатов (в противном случае вам понадобится HashMap/Table для всего результирующего набора, который может быть не так уж плох).

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

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

теперь количество запросов, которые вы делаете, зависит от количества столбцов OneToMany.

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

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

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

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

возьмите пример Matt Solnit, представьте, что вы определяете ассоциацию между автомобилем и колесами как ленивый, и вам нужны некоторые поля колес. Это означает, что после первого выбора hibernate будет делать "Select * from Wheels where car_id = :id" для каждого автомобиля.

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

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

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

Comments

    Ничего не найдено.