2014-10-07 2 views
4

Я довольно неопытен с базами данных и только что прочитал о "n+1 selects issue". Мой вопрос о последующих действиях: Предполагая, что база данных находится на том же компьютере, что и моя программа, кэшируется в ОЗУ и правильно индексируется, почему шаблон запроса n + 1 замедляется?Почему n + 1 выбирает шаблон медленным?

В качестве примера давайте возьмем код из принятого ответа:

SELECT * FROM Cars; 

/* for each car */ 
SELECT * FROM Wheel WHERE CarId = ? 

С моей ментальной моделью кэша базы данных, каждый из SELECT * FROM Wheel WHERE CarId = ? запросов должен нуждаться:

  • 1 поиск в доведите до стола «Колесо» (один хэш-код get())
  • 1 поиск, чтобы попасть в список k колес с указанным CarId (другой хэш-код get())
  • K поисков, чтобы получить колесо строк для каждого согласующего колеса (K указатель dereferenciations)

Даже если умножить на небольшой постоянный множитель для дополнительных накладных расходов из-за внутреннюю структуру памяти, она по-прежнему должен быть незаметно быстрым. Является ли межпроцессное общение узким местом?


Edit: Я только что нашел эту статью по теме через Hacker News: Following a Select Statement Through Postgres Internals. - HN discussion thread.

Edit 2: Для того, чтобы уточнить, я сделатьN предположить, чтобы быть большим. Нетривиальные служебные данные будут содержать заметную задержку, тогда да. Я спрашиваю , почему накладные расходы являются нетривиальными в первую очередь для описанной выше настройки.

+0

Поскольку один оператор извлечения п + 1 строк почти всегда быстрее, чем п + 1 заявления. При анализе SQL возникают проблемы, подготовка плана выполнения и последующее возвращение данных (в зависимости от СУБД, этап синтаксического анализа может быть существенным издержками). И кроме того: читали бы вы рецепт для торта, а затем отправляетесь в супермаркет, чтобы купить муку, идите домой, прочитайте рецепт, вернитесь, купите яйца, идите домой, прочитайте рецепт, вернитесь купить масло, ... –

+0

«Поскольку одно утверждение, извлекающее n + 1 строки, почти всегда быстрее, чем n + 1 операторов.«Я вижу это, но я удивлен, что это занимает намного больше времени, когда люди, пишущие свои программы в Ruby или Python, а не C, действительно заботятся. Решения, которые я нашел, иногда выглядят значительно сложнее исходного кода. если бы ингредиенты были только в другой комнате, мне бы, наверное, было бы не слишком важно переходить дважды или трижды;). – Perseids

+0

Повторный анализ синтаксического анализа: учитывая, что мы уже используем подготовленные заявления повсюду, разве это не то, что хорошо развитая библиотека должна (@a_horse_with_no_name) – Perseids

ответ

5

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

Если мы находимся на одной машине, задержка связи на несколько порядков меньше, но синхронная связь с другим процессом обязательно включает в себя переключатель контекста, который обычно стоит> 0,01 мс (source), что составляет десятки тысяч тактов.

Кроме того, как инструмент ORM, так и база данных будут иметь некоторые накладные расходы для каждого запроса.

В заключение, избегая выбора n + 1, гораздо менее важно, если база данных является локальной, но все еще имеет значение, если n велико.

1

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

База данных также потребует много блокировки и блокировки для каждого отдельного запроса. Контекстные коммутаторы уже упомянуты meriton. Если вы не используете окружающую транзакцию, она также должна строить неявные транзакции для каждого запроса. Некоторые служебные данные синтаксического анализа по-прежнему существуют, даже с параметризованным, подготовленным запросом или одним запомненным равенством строки (с параметрами).

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

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

Кроме того, рассмотрите возможность иметь более двух уровней данных. Представьте себе три уровня: блоги, записи, комментарии, со 100 блогами, каждая из которых содержит 10 записей и 10 комментариев по каждой записи (для средних). Это SELECT 1 + N + (NxM) ситуация. Для получения записей в блоге потребуется 100 запросов, а еще 1000 - для получения комментариев. Некоторые более сложные данные, и вы столкнетесь с 10000 или даже 100000.

Конечно, плохое программирование может работать в некоторых случаях и в некоторой степени. Если база данных всегда будет находиться на одной машине, никто ее не использует, а количество автомобилей никогда не превышает 100, даже очень малооптимальной программы может быть достаточно. Но остерегайтесь того дня, когда изменится любое из этих предварительных условий: рефакторинг всего этого займет у вас гораздо больше времени, чем правильное выполнение в начале. И, скорее всего, вы сначала попробуете некоторые обходные пути: еще несколько предложений IF, кеш памяти и т. П., Которые помогают в начале, но еще больше испортили ваш код. В конце концов, вы можете застрять в позиции «никогда не касаться бегущей системы», где производительность системы становится все менее приемлемой, но рефакторинг слишком рискован и намного сложнее, чем изменение правильного кода.

Кроме того, хороший ORM предлагает Вам путь вокруг N + 1: (N) Hibernate, например, позволяет указать пакетный размер (слияние многих SELECT * FROM Wheels WHERE CarId=? запросов в один SELECT * FROM Wheels WHERE CarId IN (?, ?, ..., ?)) или использовать подзапрос (например: SELECT * FROM Wheels WHERE CarId IN (SELECT Id FROM Cars)).

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

0

Все еще накладные расходы, даже если база данных находится на одной машине, кэшируется в ОЗУ и правильно индексируется. Размер этих накладных расходов будет зависеть от того, какую СУБД вы используете, на машине, на которой она работает, количества пользователей, конфигурации СУБД (уровень изоляции и т. Д.) И т. Д.

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

В один прекрасный день кто-то может захотеть поместить базу данных на отдельную машину или использовать разные dbms. Это часто случается в деловом мире (чтобы соответствовать стандарту ISO, уменьшать затраты, изменять поставщиков, ...)

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

Все это во многом зависит от того, для чего предназначено программное обеспечение. Избежать «выбрать проблему n + 1» не всегда необходимо, это просто эмпирическое правило, чтобы избежать часто встречающейся ловушки.

+0

Прошу прощения, но вы на самом деле не отвечаете на мой вопрос. le perhead за запрос, то он будет складываться для больших 'n', так много ясно. Я также вижу, что для многих приложений вы хотите в будущем доказать, что ваш код будет достаточно быстрым для нелокальных баз данных. Однако есть случаи, когда это никогда не будет необходимо, подумайте о sqlite DBs, например. Вы подразумеваете, что шаблон n + 1 выбирает * не * медленно в этих случаях (используя правильную базу данных)? – Perseids

+0

Я пытаюсь объяснить, что нет черного и белого ответа на этот вопрос. ** n ** всегда будет медленнее, чем ** 1 **, но в некоторых случаях ** n ** может быть достаточно быстрым. Это зависит. Встроенная база данных SQLite является одним из тех случаев, когда ** n **, вероятно, останется достаточно маленьким. –

+0

См. Изменение, чтобы прояснить вопрос. В качестве примера в реальной жизни рассмотрим mpd, музыкальный плеер, в котором есть база данных локальных музыкальных файлов, которая будет содержать тысячи записей. Многосекундная задержка при поиске песен на моем малине pi свидетельствует о том, что n является «большим» и что шаблон доступа неэффективен. В некотором смысле мир здесь очень черный. (Предположим, что фактическая причина медленного поиска с mpd - недостающий индекс, хотя, но вы поняли суть.) – Perseids

3

Предполагая, что база данных находится на том же компьютере, что и моя программа

Никогда считать это. Размышление об особых случаях, подобных этому, никогда не является хорошей идеей. Вполне вероятно, что ваши данные будут расти, и вам нужно будет поместить свою базу данных на другой сервер.Или вам понадобится избыточность, которая включает (вы уже догадались) другой сервер. Или для обеспечения безопасности вы можете не захотеть, чтобы ваш сервер приложений находился в том же поле, что и БД.

Почему шаблон запроса n + 1 медленный?

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

1) ОЗУ ужасно медленно. Ваш процессор тратит около 200-400 циклов процессора каждый раз, когда ему нужно что-то прочитать из ОЗУ. У CPU есть много трюков, чтобы скрыть это (кеши, конвейерная обработка, гиперпоточность)

2) Чтение из ОЗУ не «Случайный доступ». Это похоже на жесткий диск: последовательные чтения быстрее. Смотрите эту статью о том, как доступ к оперативной памяти в правильном порядке, это 76,6% быстрее http://lwn.net/Articles/255364/ (Читать всю статью, если вы хотите знать, как ужасающе комплекс RAM на самом деле.)

кэш процессора

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

Случай «1 запрос», вероятно, включает в себя одиночный замкнутый цикл на сервере (поиск и копирование каждой строки), а затем один закрытый цикл на клиенте (чтение каждой строки). Если эти петли достаточно малы, они могут выполнять 10-100 раз быстрее, чем кеш.

RAM последовательный доступ

«1 запрос» дело будет читать все из БДА на один линейный буфер, отправить его клиенту, который будет читать его линейно. При передаче данных не происходит произвольного доступа.

«Случай запроса N + 1» будет выделять и де-распределять ОЗУ N раз, что (по разным причинам) может быть не таким же физическим битом ОЗУ.

Различные другие причины

Подсистема сети требуется только для чтения одного или двух заголовков TCP, вместо N.

Ваш DB нужно только разобрать один запрос вместо N.

Когда вы бросаете многопользовательских пользователей, «локальность/последовательный доступ» становится еще более фрагментированным в случае N + 1, но остается довольно хорошим в случае с 1-запросом.

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

См: http://blogs.msdn.com/b/oldnewthing/archive/2014/06/13/10533875.aspx

 Смежные вопросы

  • Нет связанных вопросов^_^