N+1 и общая оптимизация запросов

Теперь я знаю, что я не умею писать эффективные запросы. Я начал эту неделю с того, что мой руководитель прокомментировал, что код, который я внедрил, загружался 5 секунд, потому что он вызывал до 1000 запросов. 1000? Невозможно. Это не мой код. Действительно ли это так? (спойлер: это так) Позвольте мне объяснить.

Активная запись

Активная запись в Rails — это жемчужина, на которую стоит посмотреть (каламбур), но с большой силой… Рассмотрим эти два фрагмента кода

users = User.all
users.count
Войти в полноэкранный режим Выход из полноэкранного режима
users = User.all
users.size
Войти в полноэкранный режим Выход из полноэкранного режима

Как вы думаете, какой из них будет выполняться быстрее? Они оба предварительно загружены, поэтому вторая строка в любом из них должна выполняться за постоянное время O(1), не так ли? НЕПРАВИЛЬНО

Count всегда будет вызывать запрос к базе данных, потому что он подсчитывает количество элементов, используя SQL-запрос (SELECT COUNT(*) FROM...).

Size, с другой стороны, вызовет запрос к БД только в том случае, если он будет вызван для чего-то, что еще не было загружено. Если он был загружен, то size будет работать в памяти.
Почетное место здесь занимает length, которая вернет значение, только если будет вызвана на предварительно загруженном элементе. Подробнее здесь. Это небольшой всплеск, но эти запросы увеличиваются, если вы используете их, скажем, в цикле.

N + 1

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

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

Как же получить все с первого захода в магазин? ActiveRecord снова приходит на помощь.

Include, Preload и EagerLoad

Здесь я буду ссылаться на интерфейс запросов Active Record.

Рассмотрим следующий пример

books = Book.limit(10)

books.each do |book|
  puts book.author.last_name
end
Вход в полноэкранный режим Выход из полноэкранного режима

Для каждой книги в цикле есть запрос, чтобы получить связанного автора и захватить его фамилию. Для 10 книг это 11 запросов. (10 + 1 … N + 1). Проблема здесь заключается в сложности размера этого запроса, O(N). Почему это плохо? Ну, память — это главная недвижимость в производстве. Если ваш сервер занят захватом пары миллионов записей, есть шанс, что он больше никого не обслуживает.

Includes, Preload и eager load являются решениями этой проблемы.
Includes => Active Record гарантирует, что все указанные ассоциации будут загружены с использованием минимально возможного количества запросов.

books = Book.includes(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end
Вход в полноэкранный режим Выход из полноэкранного режима

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

Preload => Active Record загружает каждую указанную ассоциацию, используя один запрос на ассоциацию.

Eager Load => Active Record загружает все указанные ассоциации с помощью LEFT OUTER JOIN.

Почему бы просто не загрузить все это в память?

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

# we have a million users and limited memory
User.all.each do |user|

Вход в полноэкранный режим Выйти из полноэкранного режима

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

User.find_each() do |user|
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Заключение

Мне еще есть над чем работать в плане навыков работы с БД. Надеюсь, что добавление этих навыков к моему набору умений поможет мне писать более производительный код. Есть советы, как делать запросы лучше? Поделитесь с классом в комментариях ниже.

Оцените статью
devanswers.ru
Добавить комментарий