Что такое N+1 Выберите вопрос запроса?
SELECT N+1 обычно указывается как проблема в обсуждениях объектно-реляционного отображения (ORM), и я понимаю, что это связано с необходимостью делать много запросов к базе данных для чего-то, что кажется простым в объектном мире.
У кого-нибудь есть более подробное объяснение проблемы?
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 1O / 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.colourORM с радостью вернет всех людей (обычно как экземпляры объекта 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.
гораздо быстрее выдать 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